From 4d1468f82c71ab0a31baed2e038b47402b47d9d2 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 10:41:31 +0100 Subject: [PATCH 01/35] Created a multi step wizard for the configuration of Split Payments --- manifest.json | 2 +- package.json | 2 +- static/image/icon-wallet.png | Bin 0 -> 7185 bytes static/js/index.js | 513 +++++++++++++++++++++++- templates/splitpayments/index.html | 600 ++++++++++++++++++++++++----- 5 files changed, 1014 insertions(+), 103 deletions(-) create mode 100644 static/image/icon-wallet.png diff --git a/manifest.json b/manifest.json index 3ee75e1..c41230c 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "repos": [ { "id": "splitpayments", - "organisation": "lnbits", + "organisation": "blackcoffeexbt", "repository": "splitpayments" } ] diff --git a/package.json b/package.json index 8af073b..b0535cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitpayments", - "version": "1.0.0", + "version": "1.0.1", "description": "", "main": "index.js", "scripts": { diff --git a/static/image/icon-wallet.png b/static/image/icon-wallet.png new file mode 100644 index 0000000000000000000000000000000000000000..5a1951548f959092164bc178ee8a791995adf2f5 GIT binary patch literal 7185 zcmaJ`cRXBOw^pKr(W3WWqK#fgNkoeeIiN-Mhy`qGDz@7?=`wXA{Y^M z)Chv;qC{{f-~Hb2-aqbp&+m73S$jRtUVE=~&ffc+cvEA28Y&JdA|fIhgS$HBgnX88 zk5G~mQd@g2YeL43)U`!gc)KBkoc*9gTCUzMP(A~Mvpdur>g*cg`vIy>L_{j)VQGuB z1sj1N-Uw;uKRVLE2pfx3HmX$b7Lbqer#xM~R4D1v3cKH5;2 z$K6mrs70u;B_z}nqUtJeOOsDM7(^gIK#|UT!3emQKPXs3;9tBTLivvvD8TnG6{M$z zz<(QM3pVA`_V$DFsY=UAL1bj)`IJ?qRh3j_oK=+FAd-A?GIEMQ8F`?Ll9ZebNKpnP ztHk&3OMuX!pQ{_lTu1NU-4SXU0x%@f2LuEL1qDe5$xD0txdUZYRaJp9azHsbDT0QS ze~1^-IatceU+`}R9jHIV&%+1l;qAruhtb)^I{>L6Krr^dM}hG953QH~zh{atXTV@* zAE2zX%%4&Htq2DH|EdVYf3*FP=FtDK_x~jJw+!)t0?ncR-T{6Pg74e}|8(U8()NQo zBfb4By}jXoPoyc#8|m#2^Y-DBQ<7HU69YR#JiPu$ZvG1b27?T|{E^OH5U7EUh5&&? z+QY*Yq%5PWrJ^b$uc&fITUJ(2MnzXqPDf8pRar+)ML}0i@o%h-HzWW7^+NuQb^RZ# z!arjF1O&o|(6bKI&m$1(s^{m8;QQC8K_35Xi|jx0{Tu80&$h_@BNj+72KdLf|I62Z ziwFVqr}&@XB~<s++*vWahaOp-^kn!z{xT@%e#e3^do35p~q7e?qY z5zjCXlX1FkHMVV3dOfN->u{JbZMU2#D?Q(wKUp}rwZ7Kyc5}jEXZP_?+euq;usUsM zX*l+XY7Rw>XM(Rkl;~bFzFah>X^Zpt{YqR8zyHx%DC7ygi5QNUHg-8@!s|~QPHi{f zb$tiPX}SxIk4>hfd#y8nuX8-Cxnci;5$kCjVaexrrTgl3O}k%CEFAB(HVtNkQO=|{ zzq@7}Se@7a#|Xlka`%_=IQYUil&t{lLl4tL6$P?a8(Cn%<|m|`tdMtPc)*XBikZYT zCTq`{*@rU|i}>Kw)laOmH}dB___m(qVycB9HvybQzt?l;3#Hr#ZA&??D^PYSROt3b zUr7n|zpk+r_oU5&W$FY`gz_}Dpm^;>OeM3Km04lIxD;&{9~m^1rOetx4{c5J3hJ6| z1mg=w+B{~7QLFC^Me%n{5*&8M1$$vqT+M@vX(K9y;=m0W|s`3-o0|0`z-`@eI_{#yJg9; zOjkMpqej-cU7L{)j`VgLw7uD7)~Q0|E*kO?rz?)}SAz*6qu|63zSy*Q*0e?h>6D++M-^X=7OqG*+AhW-i4+{uXjp zTWk9(N{{&Fs5_*ll^EQ3t`Y;3Ko`|*QO1L4oA!#JHAfAwZ=eUmFEg>9SryEQDmYT; zookRtZi9LbF&VgIW(P}>P;4JPe7bVoaJaVah=<*;m|mjim~`&Ft;Uo2dmTR{)uOBu z$N0B@-h~x6QOe9*i}c)w{2JS{ZY3^fHIpsY(-=kci(BxwGxky(blomB2Tb-2ZT0pF zENH7Ns};5+tay}FTjU>b)1yT<0^h|d^iQfQc;Q(a*>9)MQs#5wi&DZ(m)6+Ecrvvm zOJ-h{J|w8TiB?G-6B=6n>e=OjE^hi*GDqItaiwl~AEkC0>qItE+krBJ7ye=%A8$?Z ziUn7yUz9|Yxun8`1*;gYSbp-x?8R5mYqo&vZ5_Cs#J1w)QVjXcUNt!2rUNoo6+rOPvp0zwMYHYp zDaAGNtfMQ6T>v>+`*xwYMvv-LixFF;#g;_|@En+Y6jYpCWP-}%6DCP(ozI@vb7F1| zr8U_y4&)}wi zw;02Xdd3!lLFcl1oYp(os%k=Tk~c75X_P3fXmeN!3r+tg16%bgGoMiLdIt=n{Q|}p z<>mtY1hT)>un*eL28rfa3&E*B2AY0C2T@ZSRv_oz>y%IZj=5W@;-B^H z<_!rhj4bS9#N?BNgMsg*Tq5>f`wM|PlRrSEf@vFq5}(7$*(J*%8eWa#7FPJSec@fuyn^@85rJ6xkFFU-n7Cvk7dsE`R&0zStLHj-Fj zKoP2OUXe=JfgVo}$CRA=>dznAypO}T1a^?R&U^$mlikz18q63lu^Z~@p5m|GebO9$ z*QT1X*rew1pLdL!9yeeLP>{^wMRt|Bs4e-01j^_ovlvZM;kz--tG$us$sZitUOk=G ze-RkFMc=qiU!G#6Dnl-%_!|Eca2h-MF{JMqs86+985s*J5Zu+?I=&TeL!m^RIQM>l zp;A$thc9}vyIz++7?yOi!rnb(v<2`!x|6o#q>}`phKULu^oCLgDw8zouaZJ>pbzH4 zcwEnmr@BEb97q~|eQZ^G&vM>%%$;3Cwir9}5;iQolW!+c%yt?F*-%`16 zcV!ZmNA`N-gCh;6LJ5xOOq!F(wo$e{OA{lb8efi}4~BXdx4EXH7)`Ii2(p8FLMH0w836C3_VU7XLovXP_G%hIk$N|e!ai??fuvCBfi`o^uIAHdg zE9Er2?pUG`_DuR~N#u!2y(n7LZ}y6V$UqWvzR6pkbd-HSx-c2|Y3O3DW4 zzlW>}`fOHU-Kyc&>?rwjy*E3sEK(CW;ODKK@D+xYvHZKHr^;769M+@J7M?6xOLd*h zP)p73$(4h1ODF&HqPN1i-p3tStc8VEy4eAru;!b0KvfiRPe7YaVD<6~&w>E_lf3Mb zVkdPF@kfz`zv>5|Zb4~U66Pp9LE{4#g)bx}w?c?H9$fgkm?G6n@v1MAI%(%u{jCDn zS?!CweG`jqP}Vl5DB04iiN+NJ%8AMtp2cPdUOp~kG0c1VoT;m_N2wLMp4Pyyfu9n^ zC;h{)y8ys(@l)gN)qO!+jb*sd>h72B{+*0bi^o({fwy`fv~Nk1DsrUvv){*OrxlMD zY*gstR%*k#)b?C9)_1s(H=D(u-KlS}&OVRiK2Eu@Z2vSDJs2T+9+vcgj`Ft@$!`1z z@Uv=rl|D*u-`aRTJ65BRbN2U69_}zMiEUhe6N$fGt0(@);6?hwc zSLZDMEQLLQlS;N|Ju5#q1VNKyw<5orIy4p_nZ|jjOG2+mTb~rtn$!;dJR#8dI%O>r z&YW-qqk7*ou;aJ5V_MZQ<5n(+o#%A0<~(Mr-Nbh~@nr-U5RWPnm1FxyopP_st5cUe9riR-^0tcSRa zg-(J?5L!bX?0EHjPbVHK5lVm2d;S*Bk`*3${sOpMzM(G?x11k-T`XL@$A7yl>R0Rt zlQ6}R<-Pr6t&e+pw;Azi1Dd9*fYJd z(QCBq_CJ(eqIX#8wgH}2nRCLdnK@>U<`4cFdx1XOd?Iph8$D1W#h=cN$+qf__P+hy zus5CWEEecy-yrB>;#mwqx#g%`O^7Zn9;pe>m7+Q$ zEp#FyW0`jPR$t|{=di#9;;ViN%g9gCR5P>+ZT;>w<3Dk&KV^w?Vq7y-h7oNw$${um zn1~XTh4ZTQw%y>SjZvq}XqA`3(Bo>yP^UoD!h2ZS9}`lD6Cfy@hd+~df?cy(y8$IGK^-}C!E0{36RE(33za2WSed(|FmO{FXt3& z(vpSU=FYlLT2bFBw!XP3%O+lOp=RF@)ZKY@E8u3UxLM5mn#JoP+0F>gG7&Y+H^jYm z^UXdIPm|x%U4m2MR1)oEP3TX3}2ZAJ5-Zd%{vjWyxY+jKbWe>z;uIy=Z_n zisy@~X%0Q+N2|2@V-h${Xv)^_9Jb;a1?9i~0x^$#*2MYuN1MuhZ4)2xFg!RDXiY&h z7laA||MD7B(HxT3lz`dUKb1hj(`}mz_p7F~mb%Km_id?kOfE+y7={b8B-&W69A3E8 zCN|qFSGr@&$+2;x2eFMmn|-7>uL3W!r;UMDMvhb+1vaIdnj_*%Tiz0zcPjhVn$qyL zzfUE3mvR;_lwsk!(vGaYRn12>tr^;>OPUMz5msS<8^CIt(m^@j{W_~BH zZyQx%cD!5r2u)qjW9WMbuTAfK8=>g%nKow?qVQs+F7fO+>~{0>u}s0%ADDy-F^;l} z_gy0wRE;yI?*2Da1SPpH);=F!!JK~NtjI^N)YVtVI21rhE9fOU*9y!0D1%Ey{|amy zEFIRFrWz3tfH#Rm;9bp3tvpbsQASU$1b;PiJZb+qNWQLR6wmJi;c@YfqDdye^9A2;Q) zD~$i;`h_kgz%Te?m)dvpF`a81>cT|%%w~mPjPo;fnjOt`?Tynjb95R9GbWF?$Dv@H z@za}M6&N&vx#w7+Cr~iD*j?2B=t%o~wineaGPz93Fi9GNs*HX1-m8^{onp_LRJfr7Ts2=UvLl^In6ia4+-n* zk!n?DuiG&26XJ^0`ArbwBwwv11yXv#T z6>-K(0Lck?bmkkF`s<)2Y(KC5L`YBR(-d!p!~O&^yR)|ry7P4HOz-f-EN0?93ROwP zCF-RKFDw=UB)P3f#hKK99_5MU_%!_<=5ea4&Tex*4_bS^vf$2YjNbQ=)N`;=fRq>c;234G9fp&^zH&# zuYzxj+wncN2ZuEPz4*(=Vt0N=-E`FVoA`c_z3G>ELc!aR5uf4u6Q(-<*Oge`-mY!F z)_dC&h$k=h&bpreDy6i|#9dkTx~wgdYo1e)!KZ6fe zhp*+-%9mp;PKM}oJX2=a)Y(~;$O+rxGmwwm)9&$!9B$P3Ct#`7`Ulj9)VXd6Qj?!L zdw^!sPo;q;9Atq`WvyrzXBhrW!HMEAQ;iVJvD-5mSCdqDeGhiMC_KH(aH#-mQ4NEQ zO*EdX#x>+U6ta{J$RtY1GzA^Jh6%YFcz;(t42n(3RrukHucn(lRv&0zAbJgF#`X1v z`-s2tn>sHTKe*KB32{{QMoV%SF}eKYg)kkx7|*PPxQ~6zwP!|D z#!MoH_SKvWg{({Ua=s)q-8l>k(r)%*h2(d-05~EiO~M`z?8QrQ%jGBxzKmc*bXl66 z>IaPNRb`&+++zW{u+!vdOzQiGbUSKjQs6+DQuzj$?%4*yfIj}x8kOKBw@Kl@_u6}@ zZi=vRg!B~FeDUE%FuYmiz(6G&==Jwrt&(P_nb_;rBhDG=J>~}HUpwk!SLA7+)P7_y z=aiML$VLiI%huXBVNaJ>y|wq2I4P%zcXnO@Wa;ZW(*3I49EzgxN58QIaV*%kuS~>K!RokYDWc0Tf=1L)JVidRJl}c)- zX1>F|-zQ4Mjnvv5Ciy(?|VxWRy7x1)Prq$k(MLybKd?Hx{MBS@++MS6*D zk8wJvz;N39?JuI5kQpt0d{kx_s&P2=G!bfH;XZU)epdaLHFlp}!UW3I?!9rOIp8j7 zdJr>^J{6-@}#%z{9OUPTQhL6qbHjn#tf|A?H- zst@=ZU!ZO(#1%KgxP*Bt@gJs!9(=TQnonA*Yz)UDrFYA;N+u;g-{;O|HjYeA-M%V|?5Jcs2T`EjeYq}8Qk!s4qGhx${wM_J{-`9)oUTq;^UwW6+7 z{G^>?uA+911IAw8ike$A&M=B7I$IWs#Cia~MDSS4tJ{XeI}heeRmDp46<_XNABu{o z(C@nB^DGI8%p{p}Y|idsNrp0XiET7pCl0%PJVH$(MW#~gb+l6fTE(!dXWEsszL0Q8 z<0>yezm1rr2_E-31jD0ith#4(H@;n7c6 z_LSu0@V=_>tIkhh{QkT(UYEzBRZ>zdt)zH0N}GH((eo=30MTjJ!#7U^m!kjtq%+Vp L)~USnFzUYm-%?hh literal 0 HcmV?d00001 diff --git a/static/js/index.js b/static/js/index.js index 76b6fab..5027b3e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -19,21 +19,422 @@ window.app = Vue.createApp({ watch: { selectedWallet() { this.getTargets() + }, + splitDiagramData() { + // Recreate flow charts when data changes + this.$nextTick(() => { + this.recreateCharts() + }) + }, + currentStep() { + this.$nextTick(() => { + this.initFlowChart() + }) } }, data() { return { + // Wizard state + currentStep: 1, + maxSteps: 3, + + // Existing data selectedWallet: null, currentHash: '', // a string that must match if the edit data is unchanged - targets: [] + targets: [], + + // Chart instances + treeChart: null, + treeChart2: null, + chartUpdateTimeout: null } }, computed: { + // Step validation + canProceedFromStep1() { + return this.selectedWallet !== null + }, + canProceedFromStep2() { + return this.targets.length > 0 && this.totalPercent <= 100 && this.allTargetsValid + }, + totalPercent() { + return this.targets.reduce((sum, target) => sum + (target.percent || 0), 0) + }, + remainingPercent() { + return Math.max(0, 100 - this.totalPercent) + }, + allTargetsValid() { + return this.targets.every(target => + target.wallet && target.wallet.trim() !== '' && + target.percent > 0 && target.percent <= 100 && + target.alias && target.alias.trim() !== '' && target.alias.trim().length <= 50 + ) + }, + hasValidationErrors() { + return this.targets.some(target => + !target.wallet || target.wallet.trim() === '' || + !target.alias || target.alias.trim() === '' || + target.percent <= 0 || target.percent > 100 + ) + }, + validationSummary() { + const errors = [] + if (this.targets.length === 0) { + errors.push('At least one split target is required') + } + if (this.totalPercent > 100) { + errors.push(`Total percentage (${this.totalPercent}%) exceeds 100%`) + } + if (this.hasValidationErrors) { + errors.push('Some fields have validation errors') + } + return errors + }, + showPercentWarning() { + return this.totalPercent > 90 && this.totalPercent < 100 + }, + showPercentError() { + return this.totalPercent > 100 + }, + // Split diagram data + splitDiagramData() { + const data = [] + + // Add source wallet (remaining percentage) + if (this.remainingPercent > 0) { + data.push({ + name: this.selectedWallet ? this.selectedWallet.name : 'Source', + percent: this.remainingPercent, + type: 'source', + color: '#1976d2' + }) + } + + // Add target wallets + this.targets.forEach(target => { + if (target.percent > 0 && target.alias) { + data.push({ + name: target.alias, + percent: target.percent, + type: 'target', + color: '#43a047' + }) + } + }) + + return data.sort((a, b) => b.percent - a.percent) + }, isDirty() { return hashTargets(this.targets) !== this.currentHash } }, methods: { + // Wizard navigation + nextStep() { + if (this.currentStep < this.maxSteps) { + if (this.currentStep === 1 && this.canProceedFromStep1) { + this.currentStep++ + } else if (this.currentStep === 2 && this.canProceedFromStep2) { + this.currentStep++ + } + } + }, + prevStep() { + if (this.currentStep > 1) { + this.currentStep-- + } + }, + goToStep(step) { + if (step >= 1 && step <= this.maxSteps) { + this.currentStep = step + } + }, + + // SVG Flow Chart methods + initFlowChart() { + console.log('initFlowChart called, currentStep:', this.currentStep) + console.log('splitDiagramData:', this.splitDiagramData) + + // Create chart for Step 2 + if (this.$refs.flowChart && this.currentStep === 2) { + console.log('Creating flow chart for Step 2') + this.createFlowChart('flowChart') + } + + // Create chart for Step 3 + if (this.$refs.flowChart2 && this.currentStep === 3) { + console.log('Creating flow chart for Step 3') + this.createFlowChart('flowChart2') + } + }, + recreateCharts() { + // Safely recreate charts when data changes + if (this.currentStep === 2 && this.splitDiagramData.length > 0) { + this.$nextTick(() => { + this.createFlowChart('flowChart') + }) + } + if (this.currentStep === 3 && this.splitDiagramData.length > 0) { + this.$nextTick(() => { + this.createFlowChart('flowChart2') + }) + } + }, + createFlowChart(containerRef) { + try { + if (!this.$refs[containerRef]) { + console.warn('Container ref not found:', containerRef) + return + } + + const container = this.$refs[containerRef] + + // Clear previous content + container.innerHTML = '' + + // Create SVG element + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('width', '100%') + svg.setAttribute('height', '400') + svg.setAttribute('viewBox', '0 0 400 400') + svg.style.background = 'transparent' + + // Get targets data + const targets = this.splitDiagramData.filter(item => item.type === 'target') + + if (targets.length === 0) { + container.appendChild(svg) + return + } + + // Define positions + const sourceX = 200 + const sourceY = 80 + const branchY = 200 + const targetY = 320 + + // Calculate target positions to fill horizontal space + const targetPositions = [] + if (targets.length === 1) { + targetPositions.push({ x: sourceX, y: targetY }) + } else { + // Use the full width of the SVG viewBox (400px) with padding + const padding = 10 // Padding from edges + const totalWidth = 400 - (padding * 2) // Available width + const spacing = totalWidth / (targets.length - 1) + const startX = padding + + targets.forEach((target, index) => { + targetPositions.push({ x: startX + (index * spacing), y: targetY }) + }) + } + + // Calculate proportional line thickness + const maxPercent = Math.max(...targets.map(t => t.percent)) + const maxThickness = 30 // Maximum line thickness in pixels + + // Draw flowing lines + targets.forEach((target, index) => { + const targetPos = targetPositions[index] + // Calculate thickness proportional to the highest percentage + const lineThickness = Math.max(3, (target.percent / maxPercent) * maxThickness) + + this.drawFlowingLine(svg, sourceX, sourceY + 40, targetPos.x, targetY - 40, lineThickness, target.color || '#4ade80') + + // Add percentage label - center it on the curved line + const labelX = (sourceX + targetPos.x) / 2 + const labelY = sourceY + 40 + ((targetY - 40) - (sourceY + 40)) * 0.6 // Position at the curve peak + this.addPercentageLabel(svg, labelX, labelY, `${target.percent}%`, target.color || '#4ade80') + }) + + // Draw source wallet icon + this.drawWalletIcon(svg, sourceX, sourceY, 'source', this.remainingPercent) + + // Draw target wallet icons + targets.forEach((target, index) => { + const targetPos = targetPositions[index] + this.drawWalletIcon(svg, targetPos.x, targetPos.y, 'target', target.percent, target.name) + }) + + container.appendChild(svg) + console.log('Flow chart created successfully for:', containerRef) + } catch (error) { + console.error('Error creating flow chart:', error) + } + }, + drawFlowingLine(svg, x1, y1, x2, y2, thickness, color) { + // Create a smooth flowing path like in your reference + const midY = y1 + (y2 - y1) * 0.6 + + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + + // Create the flowing S-curve path + const pathData = ` + M ${x1} ${y1} + Q ${x1} ${midY} ${(x1 + x2) / 2} ${midY} + Q ${x2} ${midY} ${x2} ${y2} + ` + + path.setAttribute('d', pathData.trim()) + path.setAttribute('stroke', color) + path.setAttribute('stroke-width', thickness) + path.setAttribute('stroke-linecap', 'butt') + path.setAttribute('fill', 'none') + path.setAttribute('opacity', '1') + + svg.appendChild(path) + }, + + drawWalletIcon(svg, x, y, type, percentage, targetName = null) { + // Create wallet icon using the PNG image + const image = document.createElementNS('http://www.w3.org/2000/svg', 'image') + image.setAttribute('x', x - 30) + image.setAttribute('y', y - 30) + image.setAttribute('width', 60) + image.setAttribute('height', 60) + + // Try different possible paths for the wallet icon + const possiblePaths = [ + '/splitpayments/static/image/icon-wallet.png', + '/static/image/icon-wallet.png', + 'static/image/icon-wallet.png' + ] + + image.setAttribute('href', possiblePaths[0]) + + // Add color filter for source vs target distinction + if (type === 'source') { + // Add blue tint for source wallet + image.setAttribute('style', 'filter: hue-rotate(200deg) saturate(1.2)') + } + + // Add error handling - if image fails to load, show a fallback + image.addEventListener('error', () => { + console.warn('Failed to load wallet icon, using fallback') + // Remove the broken image and replace with a styled rectangle + svg.removeChild(image) + this.drawFallbackWalletIcon(svg, x, y, type, percentage, targetName) + }) + + svg.appendChild(image) + + // Add target name and percentage below icon if it's a target + if (type === 'target') { + // Add split name text + if (targetName) { + const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + nameText.setAttribute('x', x) + nameText.setAttribute('y', y + 45) + nameText.setAttribute('text-anchor', 'middle') + nameText.setAttribute('fill', '#374151') + nameText.setAttribute('font-family', 'Arial, sans-serif') + nameText.setAttribute('font-size', '14px') + nameText.setAttribute('font-weight', 'bold') + nameText.textContent = targetName + + svg.appendChild(nameText) + } + + // Add percentage text below name + const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + percentText.setAttribute('x', x) + percentText.setAttribute('y', y + 65) + percentText.setAttribute('text-anchor', 'middle') + percentText.setAttribute('fill', '#f59e0b') + percentText.setAttribute('font-family', 'Arial, sans-serif') + percentText.setAttribute('font-size', '16px') + percentText.setAttribute('font-weight', 'bold') + percentText.textContent = `${percentage}%` + + svg.appendChild(percentText) + } + }, + + drawFallbackWalletIcon(svg, x, y, type, percentage, targetName = null) { + // Fallback wallet icon when PNG fails to load + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + rect.setAttribute('x', x - 30) + rect.setAttribute('y', y - 30) + rect.setAttribute('width', 60) + rect.setAttribute('height', 60) + rect.setAttribute('rx', 12) + rect.setAttribute('fill', type === 'source' ? '#6366f1' : '#f59e0b') + rect.setAttribute('stroke', '#1f2937') + rect.setAttribute('stroke-width', 2) + + svg.appendChild(rect) + + // Add Bitcoin symbol + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text') + text.setAttribute('x', x) + text.setAttribute('y', y + 5) + text.setAttribute('text-anchor', 'middle') + text.setAttribute('fill', 'white') + text.setAttribute('font-family', 'Arial, sans-serif') + text.setAttribute('font-size', '24') + text.setAttribute('font-weight', 'bold') + text.textContent = '₿' + + svg.appendChild(text) + + // Add target name and percentage below icon if it's a target + if (type === 'target') { + // Add split name text + if (targetName) { + const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + nameText.setAttribute('x', x) + nameText.setAttribute('y', y + 45) + nameText.setAttribute('text-anchor', 'middle') + nameText.setAttribute('fill', '#374151') + nameText.setAttribute('font-family', 'Arial, sans-serif') + nameText.setAttribute('font-size', '14px') + nameText.setAttribute('font-weight', 'bold') + nameText.textContent = targetName + + svg.appendChild(nameText) + } + + // Add percentage text below name + const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + percentText.setAttribute('x', x) + percentText.setAttribute('y', y + 65) + percentText.setAttribute('text-anchor', 'middle') + percentText.setAttribute('fill', '#f59e0b') + percentText.setAttribute('font-family', 'Arial, sans-serif') + percentText.setAttribute('font-size', '16px') + percentText.setAttribute('font-weight', 'bold') + percentText.textContent = `${percentage}%` + + svg.appendChild(percentText) + } + }, + + addPercentageLabel(svg, x, y, percentage, color) { + // Create background circle for percentage + const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') + circle.setAttribute('cx', x) + circle.setAttribute('cy', y) + circle.setAttribute('r', 20) + circle.setAttribute('fill', color) + circle.setAttribute('opacity', '1') + + svg.appendChild(circle) + + // Add percentage text + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text') + text.setAttribute('x', x) + text.setAttribute('y', y + 6) + text.setAttribute('text-anchor', 'middle') + text.setAttribute('fill', 'white') + text.setAttribute('font-family', 'Arial, sans-serif') + text.setAttribute('font-size', '20px') + text.setAttribute('font-weight', 'bold') + text.textContent = percentage + + svg.appendChild(text) + }, + + // Target management methods clearTarget(index) { if (this.targets.length == 1) { return this.deleteTargets() @@ -68,17 +469,34 @@ window.app = Vue.createApp({ addTarget() { this.targets.push({ source: this.selectedWallet, - targetChoice: 'wallet' + alias: '', + wallet: '', + percent: 0 }) }, saveTargets() { - const payload = this.targets - .filter(t => t.wallet && String(t.wallet).trim() !== '') - .map(({alias, percent, wallet}) => ({ - alias, - percent: Number(percent) || 0, - wallet - })) + // Final validation before saving + if (this.validationSummary.length > 0) { + Quasar.Notify.create({ + message: 'Please fix validation errors before saving.', + timeout: 3000, + color: 'negative', + icon: 'error' + }) + return + } + + if (!this.selectedWallet) { + Quasar.Notify.create({ + message: 'Please select a source wallet.', + timeout: 3000, + color: 'negative', + icon: 'error' + }) + this.currentStep = 1 + return + } + LNbits.api .request( 'PUT', @@ -90,12 +508,31 @@ window.app = Vue.createApp({ ) .then(response => { Quasar.Notify.create({ - message: 'Split payments targets set.', - timeout: 700 + message: `Split payments activated! ${this.targets.length} target${this.targets.length !== 1 ? 's' : ''} configured.`, + timeout: 5000, + color: 'positive', + icon: 'check_circle', + actions: [ + { + label: 'Dismiss', + color: 'white', + handler: () => {} + } + ] }) + // Update hash to reflect saved state + this.currentHash = hashTargets(this.targets) + // Reset to step 1 after successful save + this.currentStep = 1 }) .catch(err => { LNbits.utils.notifyApiError(err) + Quasar.Notify.create({ + message: 'Failed to save split payment configuration. Please try again.', + timeout: 5000, + color: 'negative', + icon: 'error' + }) }) }, deleteTargets() { @@ -119,9 +556,61 @@ window.app = Vue.createApp({ LNbits.utils.notifyApiError(err) }) }) + }, + + setDefaultWallet() { + // If no wallet is selected and wallets are available + if (!this.selectedWallet && this.g.user.wallets && this.g.user.wallets.length > 0) { + // Check if any wallet has existing split payment configurations + this.checkExistingConfigurations() + } + }, + + async checkExistingConfigurations() { + // Check each wallet for existing split payment configurations + for (const wallet of this.g.user.wallets) { + try { + const response = await LNbits.api.request( + 'GET', + '/splitpayments/api/v1/targets', + wallet.adminkey + ) + if (response.data && response.data.length > 0) { + // Found existing configuration, select this wallet + this.selectedWallet = wallet + this.getTargets() + return + } + } catch (err) { + // Wallet has no configuration, continue checking others + continue + } + } + + // No existing configurations found, select first wallet + if (this.g.user.wallets.length > 0) { + this.selectedWallet = this.g.user.wallets[0] + } } }, created() { - this.selectedWallet = this.g.user.wallets[0] + // Set default wallet after ensuring data is available + this.$nextTick(() => { + this.setDefaultWallet() + }) + }, + mounted() { + this.$nextTick(() => { + this.initFlowChart() + }) + }, + beforeUnmount() { + // Clean up flow chart containers + if (this.$refs.flowChart) { + this.$refs.flowChart.innerHTML = '' + } + if (this.$refs.flowChart2) { + this.$refs.flowChart2.innerHTML = '' + } } }) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 76103f2..849fa80 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -2,41 +2,182 @@ %} {% block page %}
- - + + + +
Configure Split Payment
+ + + + + + + + +
+
+ + + + +
Select Source Wallet
+

Choose the wallet from which Bitcoin Lightning payments will be split.

+ - + +
+ No wallets available +
+
+ You need at least one wallet to configure split payments. +
+
+ + + + - + +
Select a source wallet
+
+ Choose which wallet will receive payments and automatically split them to your configured recipients. +
+
+ +
+ + {% raw %}{{ canProceedFromStep1 ? 'Continue' : 'Select Wallet to Continue' }}{% endraw %} + + +
- - -
-
Target Wallets
+ + + +
Add Targets
+

Specify the target Lightning wallets and percentage splits.

+ + +
+
+ + Total: {% raw %}{{ totalPercent }}%{% endraw %} + +
+ Remaining in source wallet: {% raw %}{{ remainingPercent }}%{% endraw %} +
+
- + + + +
High percentage warning
+
+ Splits totaling close to 100% may fail for some recipients due to Lightning routing fees. Consider reducing the total to 95% or less. +
+
+ + + +
Percentage limit exceeded
+
+ Total percentage is {% raw %}{{ totalPercent }}%{% endraw %} which exceeds 100%. Please reduce the split percentages. +
+
+ + + +
Incomplete configuration
+
+ Please complete all required fields (marked with *) before proceeding. +
+
+ + +
+ +
+ No split targets configured +
+
+ Click "Add Another Split Payment Recipient" below to get started +
+
+
@@ -57,17 +198,18 @@
Target Wallets
Target Wallets + flat + class="self-center" + > + Remove this split recipient +
-
-
- - Add Target - -
-
- - Delete all Targets - -
- -
- - Save Targets - -
+ +
+ + Add Another Split Payment Recipient +
+ + +
+
Split Preview
+
+
+
+
+ +
+ + + + {% raw %}{{ canProceedFromStep2 ? 'Continue' : (targets.length === 0 ? 'Add Targets to Continue' : allTargetsValid ? (totalPercent > 100 ? 'Fix Percentages to Continue' : 'Continue') : 'Complete Fields to Continue') }}{% endraw %} + +
+ + + + + + +
Review & Confirm
+

Review your split payment configuration and activate when ready.

+ + +
+ +
+ + +
+

+

Source Wallet

+
+
+ {% raw %}{{ selectedWallet ? selectedWallet.name : 'None selected' }}{% endraw %} +
+
+ Keeps {% raw %}{{ remainingPercent }}%{% endraw %} of payments +
+
+
+
+ + +
+ + +
+

+

Split Targets

+
+
+ {% raw %}{{ targets.length }}{% endraw %} recipient{% raw %}{{ targets.length !== 1 ? 's' : '' }}{% endraw %} +
+
+ Total {% raw %}{{ totalPercent }}%{% endraw %} of payments split +
+
+
+
+
+ + +
+
Split Configuration Details
+ + + + + {% raw %}{{ target.percent }}%{% endraw %} + + + + + {% raw %}{{ target.alias }}{% endraw %} + + + {% raw %}{{ target.wallet }}{% endraw %} + + + + +
+ + +
+
Payment Flow Preview
+
+
+
+
+ + + + +
Configuration Issues:
+
    +
  • {% raw %}{{ error }}{% endraw %}
  • +
+
+ + + + +
Configuration ready!
+
+ Your split payment configuration is valid and ready to activate. +
+
+ + + + +
Important:
+
+ • Splits totaling 100% may fail due to Lightning routing fees
+ • Each payment to this wallet will be automatically split
+ • Changes take effect immediately after activation +
+
+ + +
+ +
+ + + + + + {% raw %}{{ validationSummary.length === 0 ? 'Confirm and Activate' : 'Fix Issues to Activate' }}{% endraw %} + +
+
+
@@ -136,38 +483,113 @@
- - -

- Add some targets to the list of "Target Wallets", each with an - associated percentage. After saving, every time any payment - arrives at the "Source Wallet" that payment will be split with the - target wallets according to their percentage. -

-

- This is valid for every payment, doesn't matter how it was created. -

-

- Targets can be LNBits wallets from this LNBits instance or any valid - LNURL or LN Address. -

-

- LNURLp and LN Addresses must allow comments > 100 chars and also - have a flexible amount. -

-

- To remove a wallet from the targets list just press the X and save. - To remove all, click "Delete all Targets". -

-

- For each split via LNURLp or Lightning addresses a fee_reserve is - substracted, because of potential routing fees. -

-
-
+ +
How splits work
+

+ Add targets to split payments automatically. Every time a payment + arrives at the "Source Wallet", it will be split with the + target wallets according to their percentage. +

+

+ This works for every payment, regardless of how it was created. +

+ +
Supported formats
+ + + + + + + LNbits wallet ID + + + + + + + + LNURLp string + + + + + + + + Lightning address + + + + +
Requirements
+ + + + + + + LNURLp must allow comments > 100 chars + + + + + + + + Receiving wallet must accept flexible amounts + + + + +
Notes on fees
+

+ For each split sent to a Lightning address or LNURLp, a "Fee Reserve" is + subtracted to cover potential routing fees. This fee is not deducted from the + recipient's wallet, but from the source wallet. +

+

+ If the split total is close to 100%, the payment may fail for some recipients because of routing fees. Keep this in mind when setting up splits for a wallet where small payments are common. +

+
-{% endblock %} {% block scripts %} {{ window_vars(user) }} - +{% endblock %} + +{% block styles %} + {% endblock %} + +{% block scripts %} {{ window_vars(user) }} + +{% endblock %} \ No newline at end of file From 6731d54661655cd7a7e2f370a43aa26de9104d9e Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:52:01 +0100 Subject: [PATCH 02/35] bump v --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b0535cd..336801a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitpayments", - "version": "1.0.1", + "version": "1.0.2", "description": "", "main": "index.js", "scripts": { From e674b09850ba20f2744d3896617db6f4a4a13e60 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:16:25 +0100 Subject: [PATCH 03/35] Tweaky --- static/js/index.js | 14 ----------- templates/splitpayments/index.html | 38 ++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 5027b3e..73eb0b7 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -558,14 +558,6 @@ window.app = Vue.createApp({ }) }, - setDefaultWallet() { - // If no wallet is selected and wallets are available - if (!this.selectedWallet && this.g.user.wallets && this.g.user.wallets.length > 0) { - // Check if any wallet has existing split payment configurations - this.checkExistingConfigurations() - } - }, - async checkExistingConfigurations() { // Check each wallet for existing split payment configurations for (const wallet of this.g.user.wallets) { @@ -593,12 +585,6 @@ window.app = Vue.createApp({ } } }, - created() { - // Set default wallet after ensuring data is available - this.$nextTick(() => { - this.setDefaultWallet() - }) - }, mounted() { this.$nextTick(() => { this.initFlowChart() diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 849fa80..b52d74e 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -15,7 +15,7 @@ keep-alive header-nav > - + @@ -29,7 +29,7 @@
Select Source Wallet
-

Choose the wallet from which Bitcoin Lightning payments will be split.

+

Choose the wallet from which payments will be split.

@@ -47,7 +47,6 @@ v-model="selectedWallet" :options="g.user.wallets.map(w => ({label: w.name, value: w}))" color="primary" - class="q-gutter-x-none" type="radio" /> @@ -160,11 +159,12 @@
+
Split {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ targets.length }}{% endraw %}
+ +
(val * 10) % 1 === 0 || 'Maximum 1 decimal place allowed' ]" > - +
+
Remove this split recipient
+
-
Split Configuration Details
+
Split Payment Summary
Important:
- • Splits totaling 100% may fail due to Lightning routing fees
- • Each payment to this wallet will be automatically split
- • Changes take effect immediately after activation + • Splits totaling 100% may fail due to Lightning routing fees. Please keep this in mind when setting up splits for a wallet where small payments are common. + • Each payment to this wallet will be automatically split when it arrives
@@ -559,6 +560,17 @@
{% block styles %} {% endblock %} From 119125a9a0878944d2bcb31327a06d4787d708d8 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:02:43 +0100 Subject: [PATCH 08/35] x --- templates/splitpayments/index.html | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index ac80e9d..9e7a40e 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -180,16 +180,6 @@ -
- -
- No split targets configured -
-
- Click "Add Another Split Payment Recipient" below to get started -
-
-
Split {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ target class="text-weight-medium" no-caps > - Add Another Split Payment Recipient + Add a Split Payment Recipient + Add Another Split Payment Recipient
From 648a52381f17fb5a067854f5f1c4337ef9cc09ee Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:06:04 +0100 Subject: [PATCH 09/35] Update layout for step 2 --- templates/splitpayments/index.html | 339 +++++++++++++++++++---------- 1 file changed, 230 insertions(+), 109 deletions(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 9e7a40e..ef76015 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -100,10 +100,10 @@ - - -
Add Targets
-

Specify the recipients of the split payments and the percentage of the payment that each recipient will receive.

+ + +
Add Targets
+

Specify the recipients of the split payments and the percentage of the payment that each recipient will receive.

@@ -183,116 +183,87 @@
-
Split {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ targets.length }}{% endraw %}
- -
- -
-
- -
-
- +
+
+ {% raw %}{{ t + 1 }}{% endraw %}
+ Split Recipient {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ targets.length }}{% endraw %} + + Remove this recipient + +
+ +
+ + + + +
- - - - - -
-
- - Remove this split recipient -
-
-
+
Add a Split Payment Recipient @@ -309,14 +280,14 @@
Split {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ target
-
+
Split {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ target :color="canProceedFromStep2 ? 'primary' : 'grey-5'" @click="nextStep()" :disabled="!canProceedFromStep2" - size="md" - class="text-weight-medium q-px-lg" + size="lg" + class="text-weight-medium continue-button" no-caps icon="arrow_forward" > @@ -733,6 +704,156 @@
padding-top: 8px; } } + +/* Step 2 Mobile Improvements */ +.split-recipient-container { + border-left: 3px solid #1976d2; + padding-left: 16px; + margin-bottom: 24px; + position: relative; +} + +.split-recipient-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.split-number-circle { + width: 28px; + height: 28px; + border-radius: 50%; + background-color: #1976d2; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 14px; + flex-shrink: 0; +} + +.split-header-text { + font-size: 1rem; + font-weight: 500; + color: #374151; + flex: 1; +} + +.remove-btn { + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.remove-btn:hover { + opacity: 1; +} + +.split-fields { + display: flex; + flex-direction: column; + gap: 12px; +} + +.split-field { + width: 100%; +} + +.split-field .q-field__control { + min-height: 48px; +} + +.add-recipient-container { + display: flex; + justify-content: center; + padding: 16px 0; +} + +.add-recipient-btn { + min-height: 48px; + padding: 0 24px; +} + +.step-navigation { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 16px 0; +} + +.back-button { + min-height: 48px; + padding: 0 20px; +} + +/* Mobile-specific styles for Step 2 */ +@media (max-width: 768px) { + .split-recipient-container { + border-left: 2px solid #1976d2; + padding-left: 12px; + margin-bottom: 20px; + } + + .split-recipient-header { + gap: 8px; + margin-bottom: 12px; + } + + .split-number-circle { + width: 24px; + height: 24px; + font-size: 12px; + } + + .split-header-text { + font-size: 0.875rem; + } + + .split-fields { + gap: 8px; + } + + .split-field .q-field__control { + min-height: 52px; + } + + .add-recipient-btn { + width: 100%; + min-height: 56px; + font-size: 1rem; + } + + .step-navigation { + flex-direction: column; + gap: 12px; + padding: 20px 0; + } + + .back-button { + width: 100%; + min-height: 52px; + order: 2; + } + + .continue-button { + width: 100%; + min-height: 56px; + order: 1; + } + + /* Improve percentage badge layout on mobile */ + .items-center.q-gutter-md { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .items-center.q-gutter-md .q-badge { + align-self: flex-start; + } +} {% endblock %} From 69fdb1dd440096fd2aa79276fed43fde65e7ac13 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:17:59 +0100 Subject: [PATCH 10/35] Step 3 improvements --- templates/splitpayments/index.html | 364 +++++++++++++++++++++++------ 1 file changed, 292 insertions(+), 72 deletions(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index ef76015..a8a29eb 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -310,79 +310,72 @@ - - -
Review & Confirm
-

Review your split payment configuration and activate when ready.

+ + +
Review & Confirm
+

Review your split payment configuration and activate when ready.

-
+
-
- - -
-

-

Source Wallet

-
-
+ + +
+ + Source Wallet +
+
+
{% raw %}{{ selectedWallet ? selectedWallet.name : 'None selected' }}{% endraw %}
-
+
Keeps {% raw %}{{ remainingPercent }}%{% endraw %} of payments
- - -
+
+
+
-
- - -
-

-

Split Targets

-
-
+ + +
+ + Split Targets +
+
+
{% raw %}{{ targets.length }}{% endraw %} recipient{% raw %}{{ targets.length !== 1 ? 's' : '' }}{% endraw %}
-
+
Total {% raw %}{{ totalPercent }}%{% endraw %} of payments split
- - -
+
+
+
-
-
Split Payment Summary
- - +
Split Payment Summary
+
+
- - - {% raw %}{{ target.percent }}%{% endraw %} - - - - +
+ {% raw %}{{ target.percent }}%{% endraw %} +
+
+
{% raw %}{{ target.alias }}{% endraw %} - - +
+
{% raw %}{{ target.wallet }}{% endraw %} - - - - +
+
+
+
@@ -436,36 +429,32 @@ -
- -
- - + - - {% raw %}{{ validationSummary.length === 0 ? 'Confirm and Activate' : 'Fix Issues to Activate' }}{% endraw %} - -
+ + {% raw %}{{ validationSummary.length === 0 ? 'Confirm and Activate' : 'Fix Issues to Activate' }}{% endraw %} +
@@ -854,6 +843,237 @@
align-self: flex-start; } } + +/* Step 3 Mobile Improvements */ +.summary-cards-container { + display: flex; + flex-direction: column; + gap: 16px; +} + +/* Ensure consistent font family for all Step 3 elements */ +.summary-card-title, +.summary-card-main-text, +.summary-card-sub-text, +.section-title, +.target-name, +.target-wallet { + font-family: Roboto, "-apple-system", "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +.summary-card { + border-radius: 8px; + overflow: hidden; +} + +.summary-card-section { + padding: 16px; +} + +.summary-card-header { + display: flex; + align-items: center; + margin-bottom: 12px; + color: #1976d2; +} + +.summary-card-title { + margin-left: 10px; + margin-top: -5px; + font-weight: 600; + line-height: 1.2; +} + +.summary-card-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.summary-card-main-text { + font-size: 1.125rem; + font-weight: 700; + color: #1976d2; +} + +.summary-card-sub-text { + font-size: 0.875rem; + color: #6b7280; +} + +.bg-green-1 .summary-card-header { + color: #059669; +} + +.bg-green-1 .summary-card-main-text { + color: #059669; +} + +.detailed-targets-container { + margin-bottom: 24px; +} + +.section-title { + font-size: 1.125rem; + font-weight: 600; + color: #374151; + margin-bottom: 16px; +} + +.targets-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.target-summary-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: #f8fafc; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.target-percent-circle { + width: 60px; + height: 60px; + border-radius: 50%; + background-color: #059669; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1rem; + flex-shrink: 0; +} + +.target-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.target-name { + font-size: 1rem; + font-weight: 600; + color: #374151; +} + +.target-wallet { + font-size: 0.875rem; + color: #6b7280; + word-break: break-all; +} + +.step-3-navigation { + justify-content: center; + gap: 16px; +} + +.confirm-button { + background: #059669 !important; + color: white !important; +} + +.confirm-button:disabled { + background: #9ca3af !important; +} + +/* Mobile-specific styles for Step 3 */ +@media (max-width: 768px) { + .summary-cards-container { + gap: 12px; + } + + .summary-card-section { + padding: 12px; + } + + .summary-card-header { + margin-bottom: 8px; + } + + .summary-card-title { + font-size: 0.875rem; + line-height: 1.2; + } + + .summary-card-main-text { + font-size: 1rem; + } + + .summary-card-sub-text { + font-size: 0.75rem; + } + + .section-title { + font-size: 1rem; + margin-bottom: 12px; + } + + .targets-list { + gap: 8px; + } + + .target-summary-item { + padding: 12px; + gap: 12px; + } + + .target-percent-circle { + width: 48px; + height: 48px; + font-size: 0.875rem; + } + + .target-name { + font-size: 0.875rem; + } + + .target-wallet { + font-size: 0.75rem; + } + + .step-3-navigation { + flex-direction: column; + gap: 12px; + } + + .back-button { + width: 100%; + min-height: 52px; + order: 2; + } + + .confirm-button { + width: 100%; + min-height: 56px; + order: 1; + font-size: 1rem; + } + + /* Flow chart container mobile optimization */ + .flow-chart-container { + margin: 12px 0; + padding: 12px; + min-height: 300px; + } + + /* Status banners mobile optimization */ + .q-banner { + padding: 12px; + margin-bottom: 12px; + } + + .q-banner .q-banner__content { + font-size: 0.875rem; + line-height: 1.4; + } +} {% endblock %} From 438aff104d0fc846729b06cf775beb4fb998a31c Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:21:32 +0100 Subject: [PATCH 11/35] Add source wallet title --- static/js/index.js | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/static/js/index.js b/static/js/index.js index 96cd8e2..b87aae2 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -381,6 +381,22 @@ window.app = Vue.createApp({ svg.appendChild(image) + // Add source wallet name above icon if it's a source + if (type === 'source') { + // Add source wallet name text above the icon + const sourceNameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + sourceNameText.setAttribute('x', x) + sourceNameText.setAttribute('y', y - 45) + sourceNameText.setAttribute('text-anchor', 'middle') + sourceNameText.setAttribute('fill', '#1976d2') + sourceNameText.setAttribute('font-family', 'Arial, sans-serif') + sourceNameText.setAttribute('font-size', '14px') + sourceNameText.setAttribute('font-weight', 'bold') + sourceNameText.textContent = this.selectedWallet ? this.selectedWallet.name : 'Source Wallet' + + svg.appendChild(sourceNameText) + } + // Add target name and percentage below icon if it's a target if (type === 'target') { // Add split name text @@ -440,6 +456,22 @@ window.app = Vue.createApp({ svg.appendChild(text) + // Add source wallet name above icon if it's a source + if (type === 'source') { + // Add source wallet name text above the icon + const sourceNameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + sourceNameText.setAttribute('x', x) + sourceNameText.setAttribute('y', y - 45) + sourceNameText.setAttribute('text-anchor', 'middle') + sourceNameText.setAttribute('fill', '#1976d2') + sourceNameText.setAttribute('font-family', 'Arial, sans-serif') + sourceNameText.setAttribute('font-size', '14px') + sourceNameText.setAttribute('font-weight', 'bold') + sourceNameText.textContent = this.selectedWallet ? this.selectedWallet.name : 'Source Wallet' + + svg.appendChild(sourceNameText) + } + // Add target name and percentage below icon if it's a target if (type === 'target') { // Add split name text From 366e2fa29cccbfba1333f28a225bc9f4b737a7e1 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:25:15 +0100 Subject: [PATCH 12/35] wx --- static/js/index.js | 80 +++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index b87aae2..f0b9656 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -262,8 +262,9 @@ window.app = Vue.createApp({ svg.setAttribute('viewBox', '0 0 400 400') svg.style.background = 'transparent' - // Get targets data + // Get targets data and source data const targets = this.splitDiagramData.filter(item => item.type === 'target') + const sourceRemaining = this.splitDiagramData.filter(item => item.type === 'source') if (targets.length === 0) { container.appendChild(svg) @@ -276,47 +277,62 @@ window.app = Vue.createApp({ const branchY = 200 const targetY = 320 - // Calculate target positions to fill horizontal space - const targetPositions = [] - if (targets.length === 1) { - targetPositions.push({ x: sourceX, y: targetY }) + // Calculate bottom row items (targets + source if remaining > 0) + const bottomRowItems = [...targets] + if (sourceRemaining.length > 0 && this.remainingPercent > 0) { + bottomRowItems.push({ + name: this.selectedWallet ? this.selectedWallet.name : 'Source', + percent: this.remainingPercent, + type: 'source_remaining', + color: '#1976d2' + }) + } + + // Calculate positions for bottom row items + const bottomRowPositions = [] + if (bottomRowItems.length === 1) { + bottomRowPositions.push({ x: sourceX, y: targetY }) } else { // Use the full width of the SVG viewBox (400px) with padding const padding = 10 // Padding from edges const totalWidth = 400 - (padding * 2) // Available width - const spacing = totalWidth / (targets.length - 1) + const spacing = totalWidth / (bottomRowItems.length - 1) const startX = padding - targets.forEach((target, index) => { - targetPositions.push({ x: startX + (index * spacing), y: targetY }) + bottomRowItems.forEach((item, index) => { + bottomRowPositions.push({ x: startX + (index * spacing), y: targetY }) }) } // Calculate proportional line thickness - const maxPercent = Math.max(...targets.map(t => t.percent)) + const maxPercent = Math.max(...bottomRowItems.map(t => t.percent)) const maxThickness = 30 // Maximum line thickness in pixels - // Draw flowing lines - targets.forEach((target, index) => { - const targetPos = targetPositions[index] + // Draw flowing lines to all bottom row items + bottomRowItems.forEach((item, index) => { + const itemPos = bottomRowPositions[index] // Calculate thickness proportional to the highest percentage - const lineThickness = Math.max(3, (target.percent / maxPercent) * maxThickness) + const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - this.drawFlowingLine(svg, sourceX, sourceY + 40, targetPos.x, targetY - 40, lineThickness, target.color || '#4ade80') + this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, targetY - 40, lineThickness, item.color || '#4ade80') // Add percentage label - center it on the curved line - const labelX = (sourceX + targetPos.x) / 2 + const labelX = (sourceX + itemPos.x) / 2 const labelY = sourceY + 40 + ((targetY - 40) - (sourceY + 40)) * 0.6 // Position at the curve peak - // this.addPercentageLabel(svg, labelX, labelY, `${target.percent}%`, target.color || '#4ade80') + // this.addPercentageLabel(svg, labelX, labelY, `${item.percent}%`, item.color || '#4ade80') }) // Draw source wallet icon this.drawWalletIcon(svg, sourceX, sourceY, 'source', this.remainingPercent) - // Draw target wallet icons - targets.forEach((target, index) => { - const targetPos = targetPositions[index] - this.drawWalletIcon(svg, targetPos.x, targetPos.y, 'target', target.percent, target.name) + // Draw bottom row wallet icons + bottomRowItems.forEach((item, index) => { + const itemPos = bottomRowPositions[index] + if (item.type === 'source_remaining') { + this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'source_remaining', item.percent, item.name) + } else { + this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'target', item.percent, item.name) + } }) container.appendChild(svg) @@ -366,7 +382,7 @@ window.app = Vue.createApp({ image.setAttribute('href', possiblePaths[0]) // Add color filter for source vs target distinction - if (type === 'source') { + if (type === 'source' || type === 'source_remaining') { // Add blue tint for source wallet image.setAttribute('style', 'filter: hue-rotate(200deg) saturate(1.2)') } @@ -397,15 +413,15 @@ window.app = Vue.createApp({ svg.appendChild(sourceNameText) } - // Add target name and percentage below icon if it's a target - if (type === 'target') { - // Add split name text + // Add name and percentage below icon for targets and source_remaining + if (type === 'target' || type === 'source_remaining') { + // Add name text if (targetName) { const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') nameText.setAttribute('x', x) nameText.setAttribute('y', y + 45) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', '#374151') + nameText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#374151') nameText.setAttribute('font-family', 'Arial, sans-serif') nameText.setAttribute('font-size', '14px') nameText.setAttribute('font-weight', 'bold') @@ -419,7 +435,7 @@ window.app = Vue.createApp({ percentText.setAttribute('x', x) percentText.setAttribute('y', y + 80) percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', '#f59e0b') + percentText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#f59e0b') percentText.setAttribute('font-family', 'Arial, sans-serif') percentText.setAttribute('font-size', '32px') percentText.setAttribute('font-weight', 'bold') @@ -437,7 +453,7 @@ window.app = Vue.createApp({ rect.setAttribute('width', 60) rect.setAttribute('height', 60) rect.setAttribute('rx', 12) - rect.setAttribute('fill', type === 'source' ? '#6366f1' : '#f59e0b') + rect.setAttribute('fill', (type === 'source' || type === 'source_remaining') ? '#6366f1' : '#f59e0b') rect.setAttribute('stroke', '#1f2937') rect.setAttribute('stroke-width', 2) @@ -472,15 +488,15 @@ window.app = Vue.createApp({ svg.appendChild(sourceNameText) } - // Add target name and percentage below icon if it's a target - if (type === 'target') { - // Add split name text + // Add name and percentage below icon for targets and source_remaining + if (type === 'target' || type === 'source_remaining') { + // Add name text if (targetName) { const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') nameText.setAttribute('x', x) nameText.setAttribute('y', y + 45) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', '#374151') + nameText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#374151') nameText.setAttribute('font-family', 'Arial, sans-serif') nameText.setAttribute('font-size', '14px') nameText.setAttribute('font-weight', 'bold') @@ -494,7 +510,7 @@ window.app = Vue.createApp({ percentText.setAttribute('x', x) percentText.setAttribute('y', y + 65) percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', '#f59e0b') + percentText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#f59e0b') percentText.setAttribute('font-family', 'Arial, sans-serif') percentText.setAttribute('font-size', '16px') percentText.setAttribute('font-weight', 'bold') From ceaca27fe3b834ff6afdec4622b8b61b23d8df62 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:26:16 +0100 Subject: [PATCH 13/35] layer lines so that sourc ewallet is behind all others in chart --- static/js/index.js | 43 ++++++++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index f0b9656..29d2621 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -284,7 +284,7 @@ window.app = Vue.createApp({ name: this.selectedWallet ? this.selectedWallet.name : 'Source', percent: this.remainingPercent, type: 'source_remaining', - color: '#1976d2' + color: '#96A6FF' }) } @@ -308,18 +308,27 @@ window.app = Vue.createApp({ const maxPercent = Math.max(...bottomRowItems.map(t => t.percent)) const maxThickness = 30 // Maximum line thickness in pixels - // Draw flowing lines to all bottom row items + // Draw flowing lines - source_remaining lines first (behind other lines) + // First pass: draw source_remaining lines bottomRowItems.forEach((item, index) => { - const itemPos = bottomRowPositions[index] - // Calculate thickness proportional to the highest percentage - const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - - this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, targetY - 40, lineThickness, item.color || '#4ade80') - - // Add percentage label - center it on the curved line - const labelX = (sourceX + itemPos.x) / 2 - const labelY = sourceY + 40 + ((targetY - 40) - (sourceY + 40)) * 0.6 // Position at the curve peak - // this.addPercentageLabel(svg, labelX, labelY, `${item.percent}%`, item.color || '#4ade80') + if (item.type === 'source_remaining') { + const itemPos = bottomRowPositions[index] + // Calculate thickness proportional to the highest percentage + const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) + + this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, targetY - 40, lineThickness, item.color || '#4ade80') + } + }) + + // Second pass: draw target lines (on top of source_remaining lines) + bottomRowItems.forEach((item, index) => { + if (item.type === 'target') { + const itemPos = bottomRowPositions[index] + // Calculate thickness proportional to the highest percentage + const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) + + this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, targetY - 40, lineThickness, item.color || '#4ade80') + } }) // Draw source wallet icon @@ -421,7 +430,7 @@ window.app = Vue.createApp({ nameText.setAttribute('x', x) nameText.setAttribute('y', y + 45) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#374151') + nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') nameText.setAttribute('font-family', 'Arial, sans-serif') nameText.setAttribute('font-size', '14px') nameText.setAttribute('font-weight', 'bold') @@ -435,7 +444,7 @@ window.app = Vue.createApp({ percentText.setAttribute('x', x) percentText.setAttribute('y', y + 80) percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#f59e0b') + percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') percentText.setAttribute('font-family', 'Arial, sans-serif') percentText.setAttribute('font-size', '32px') percentText.setAttribute('font-weight', 'bold') @@ -453,7 +462,7 @@ window.app = Vue.createApp({ rect.setAttribute('width', 60) rect.setAttribute('height', 60) rect.setAttribute('rx', 12) - rect.setAttribute('fill', (type === 'source' || type === 'source_remaining') ? '#6366f1' : '#f59e0b') + rect.setAttribute('fill', (type === 'source' || type === 'source_remaining') ? (type === 'source_remaining' ? '#96A6FF' : '#6366f1') : '#f59e0b') rect.setAttribute('stroke', '#1f2937') rect.setAttribute('stroke-width', 2) @@ -496,7 +505,7 @@ window.app = Vue.createApp({ nameText.setAttribute('x', x) nameText.setAttribute('y', y + 45) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#374151') + nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') nameText.setAttribute('font-family', 'Arial, sans-serif') nameText.setAttribute('font-size', '14px') nameText.setAttribute('font-weight', 'bold') @@ -510,7 +519,7 @@ window.app = Vue.createApp({ percentText.setAttribute('x', x) percentText.setAttribute('y', y + 65) percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', type === 'source_remaining' ? '#1976d2' : '#f59e0b') + percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') percentText.setAttribute('font-family', 'Arial, sans-serif') percentText.setAttribute('font-size', '16px') percentText.setAttribute('font-weight', 'bold') From d3b5a3406e5a02bde37da0bb4fbf36d6c5d79003 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:31:23 +0100 Subject: [PATCH 14/35] Lines start at 10px thick and get thicker --- static/js/index.js | 98 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 82 insertions(+), 16 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 29d2621..0467a17 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -350,27 +350,93 @@ window.app = Vue.createApp({ console.error('Error creating flow chart:', error) } }, - drawFlowingLine(svg, x1, y1, x2, y2, thickness, color) { - // Create a smooth flowing path like in your reference + drawFlowingLine(svg, x1, y1, x2, y2, finalThickness, color) { + // Create a tapered line that starts at 10px and increases to finalThickness + const startThickness = 10 + const segments = 20 // Number of segments for smooth taper const midY = y1 + (y2 - y1) * 0.6 - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path') + // Generate points along the quadratic Bezier curve + const points = [] + for (let i = 0; i <= segments; i++) { + const t = i / segments + let x, y + + if (t <= 0.5) { + // First quadratic curve: (x1, y1) to ((x1+x2)/2, midY) + const localT = t * 2 + const p0 = {x: x1, y: y1} + const p1 = {x: x1, y: midY} + const p2 = {x: (x1 + x2) / 2, y: midY} + + x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x + y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y + } else { + // Second quadratic curve: ((x1+x2)/2, midY) to (x2, y2) + const localT = (t - 0.5) * 2 + const p0 = {x: (x1 + x2) / 2, y: midY} + const p1 = {x: x2, y: midY} + const p2 = {x: x2, y: y2} + + x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x + y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y + } + + // Calculate thickness at this point + const thickness = startThickness + (finalThickness - startThickness) * t + points.push({x, y, thickness}) + } + + // Create polygon points for the tapered line + const leftPoints = [] + const rightPoints = [] + + for (let i = 0; i < points.length; i++) { + const point = points[i] + const halfThickness = point.thickness / 2 + + // Calculate direction vector + let dx = 0, dy = 1 + if (i < points.length - 1) { + dx = points[i + 1].x - point.x + dy = points[i + 1].y - point.y + } else if (i > 0) { + dx = point.x - points[i - 1].x + dy = point.y - points[i - 1].y + } + + // Normalize direction vector + const length = Math.sqrt(dx * dx + dy * dy) + if (length > 0) { + dx /= length + dy /= length + } + + // Calculate perpendicular offset (rotate 90 degrees) + const perpX = -dy + const perpY = dx + + // Add points to left and right sides + leftPoints.push({ + x: point.x + perpX * halfThickness, + y: point.y + perpY * halfThickness + }) + rightPoints.unshift({ + x: point.x - perpX * halfThickness, + y: point.y - perpY * halfThickness + }) + } - // Create the flowing S-curve path - const pathData = ` - M ${x1} ${y1} - Q ${x1} ${midY} ${(x1 + x2) / 2} ${midY} - Q ${x2} ${midY} ${x2} ${y2} - ` + // Create polygon + const allPoints = [...leftPoints, ...rightPoints] + const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon') + const pointsString = allPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') - path.setAttribute('d', pathData.trim()) - path.setAttribute('stroke', color) - path.setAttribute('stroke-width', thickness) - path.setAttribute('stroke-linecap', 'butt') - path.setAttribute('fill', 'none') - path.setAttribute('opacity', '1') + polygon.setAttribute('points', pointsString) + polygon.setAttribute('fill', color) + polygon.setAttribute('opacity', '1') - svg.appendChild(path) + svg.appendChild(polygon) }, drawWalletIcon(svg, x, y, type, percentage, targetName = null) { From 8e5d6c5cee9c4cfbb03e6d7c92c785954dd29be4 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:34:37 +0100 Subject: [PATCH 15/35] Added arrow to end of chart lines :) --- static/js/index.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 0467a17..3373d03 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -427,8 +427,26 @@ window.app = Vue.createApp({ }) } - // Create polygon - const allPoints = [...leftPoints, ...rightPoints] + // Create arrow tip pointing down + const lastPoint = points[points.length - 1] + const arrowHeight = finalThickness * 0.8 // Arrow height proportional to final thickness + + // Get the last left and right points to connect seamlessly + const lastLeftPoint = leftPoints[leftPoints.length - 1] + const lastRightPoint = rightPoints[0] // rightPoints is reversed, so first element is the last point + + // Arrow tip points - connect directly to the line ends + const arrowTip = {x: lastPoint.x, y: lastPoint.y + arrowHeight} + + // Create combined polygon including line body and arrow + const allPoints = [ + ...leftPoints.slice(0, -1), // All left points except the last one + lastLeftPoint, // Last left point + arrowTip, // Arrow tip + lastRightPoint, // Last right point + ...rightPoints.slice(1) // All right points except the first one + ] + const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon') const pointsString = allPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') From f76ae4741bd2515be01232565a900d38e9439f82 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:38:29 +0100 Subject: [PATCH 16/35] Arrow termination fixes --- static/js/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 3373d03..7a1680a 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -316,7 +316,9 @@ window.app = Vue.createApp({ // Calculate thickness proportional to the highest percentage const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, targetY - 40, lineThickness, item.color || '#4ade80') + // End the line before the wallet icon (30px is wallet icon radius) + const lineEndY = targetY - 45 + this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') } }) @@ -327,7 +329,9 @@ window.app = Vue.createApp({ // Calculate thickness proportional to the highest percentage const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, targetY - 40, lineThickness, item.color || '#4ade80') + // End the line before the wallet icon (30px is wallet icon radius) + const lineEndY = targetY - 45 + this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') } }) From ca0fe1b4c71856a64b0fde5a30559bfb8ea40495 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 17:40:13 +0100 Subject: [PATCH 17/35] Remove console logs --- static/js/index.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 7a1680a..0c495db 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -215,18 +215,14 @@ window.app = Vue.createApp({ // SVG Flow Chart methods initFlowChart() { - console.log('initFlowChart called, currentStep:', this.currentStep) - console.log('splitDiagramData:', this.splitDiagramData) // Create chart for Step 2 if (this.$refs.flowChart && this.currentStep === 2) { - console.log('Creating flow chart for Step 2') this.createFlowChart('flowChart') } // Create chart for Step 3 if (this.$refs.flowChart2 && this.currentStep === 3) { - console.log('Creating flow chart for Step 3') this.createFlowChart('flowChart2') } }, @@ -349,7 +345,6 @@ window.app = Vue.createApp({ }) container.appendChild(svg) - console.log('Flow chart created successfully for:', containerRef) } catch (error) { console.error('Error creating flow chart:', error) } From ef59bc1cbba042a390c4e9f23958e78bea58a8d1 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 18:56:56 +0100 Subject: [PATCH 18/35] Fix config and manifest --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index c41230c..3ee75e1 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "repos": [ { "id": "splitpayments", - "organisation": "blackcoffeexbt", + "organisation": "lnbits", "repository": "splitpayments" } ] From 547914aff6bb8e0562eeae8cf90a9cb94d22fdc6 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:00:31 +0100 Subject: [PATCH 19/35] Arrows now terminate at same vertical location --- config.json | 5 +++++ static/js/index.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config.json b/config.json index 2ec4bbf..285ec7b 100644 --- a/config.json +++ b/config.json @@ -39,6 +39,11 @@ "name": "arcbtc", "uri": "https://github.com/arcbtc", "role": "Developer" + }, + { + "name": "blackcoffee", + "uri": "https://github.com/blackcoffeexbt", + "role": "Developer" } ], "images": [ diff --git a/static/js/index.js b/static/js/index.js index 0c495db..23759ce 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -428,7 +428,7 @@ window.app = Vue.createApp({ // Create arrow tip pointing down const lastPoint = points[points.length - 1] - const arrowHeight = finalThickness * 0.8 // Arrow height proportional to final thickness + const arrowHeight = 15 // Fixed arrow height so all arrows terminate at same Y position // Get the last left and right points to connect seamlessly const lastLeftPoint = leftPoints[leftPoints.length - 1] From 522f2c99aa746bfd72a44e598fee71b043c30975 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:08:33 +0100 Subject: [PATCH 20/35] Chart tweaks --- static/js/index.js | 34 ++++++++++++++++++++++++++---- templates/splitpayments/index.html | 31 +++++++++------------------ 2 files changed, 40 insertions(+), 25 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 23759ce..e793b9f 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -314,7 +314,7 @@ window.app = Vue.createApp({ // End the line before the wallet icon (30px is wallet icon radius) const lineEndY = targetY - 45 - this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') + this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') } }) @@ -327,12 +327,12 @@ window.app = Vue.createApp({ // End the line before the wallet icon (30px is wallet icon radius) const lineEndY = targetY - 45 - this.drawFlowingLine(svg, sourceX, sourceY + 40, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') + this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') } }) - // Draw source wallet icon - this.drawWalletIcon(svg, sourceX, sourceY, 'source', this.remainingPercent) + // Draw source Bitcoin logo + this.drawBitcoinLogo(svg, sourceX, sourceY) // Draw bottom row wallet icons bottomRowItems.forEach((item, index) => { @@ -612,6 +612,32 @@ window.app = Vue.createApp({ } }, + drawBitcoinLogo(svg, x, y) { + // Create Bitcoin logo using SVG + const logoGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g') + + const bitcoinPath = document.createElementNS('http://www.w3.org/2000/svg', 'path') + bitcoinPath.setAttribute('d', 'M39.0674606,19.3675957 L40.5054606,13.5995957 L36.9944606,12.7245957 L35.5944606,18.3405957 C34.6714606,18.1105957 33.7234606,17.8935957 32.7814606,17.6785957 L34.1914606,12.0255957 L30.6824606,11.1505957 L29.2434606,16.9165957 C28.4794606,16.7425957 27.7294606,16.5705957 27.0014606,16.3895957 L27.0054606,16.3715957 L22.1634606,15.1625957 L21.2294606,18.9125957 C21.2294606,18.9125957 23.8344606,19.5095957 23.7794606,19.5465957 C25.2014606,19.9015957 25.4584606,20.8425957 25.4154606,21.5885957 L23.7774606,28.1595957 L23.7714606,28.1845957 L21.4754606,37.3895957 C21.3014606,37.8215957 20.8604606,38.4695957 19.8664606,38.2235957 C19.9014606,38.2745957 17.3144606,37.5865957 17.3144606,37.5865957 L15.5714606,41.6055957 L20.1404606,42.7445957 C20.9904606,42.9575957 21.8234606,43.1805957 22.6434606,43.3905957 L21.1904606,49.2245957 L24.6974606,50.0995957 L26.1364606,44.3275957 C27.0944606,44.5875957 28.0244606,44.8275957 28.9344606,45.0535957 L27.5004606,50.7985957 L31.0114606,51.6735957 L32.4644606,45.8505957 C38.4514606,46.9835957 42.9534606,46.5265957 44.8484606,41.1115957 C46.3754606,36.7515957 44.7724606,34.2365957 41.6224606,32.5965957 C43.9164606,32.0675957 45.6444606,30.5585957 46.1054606,27.4415957 C46.7424606,23.1835957 43.5004606,20.8945957 39.0674606,19.3675957 Z M38.0834606,38.6905957 C36.9984606,43.0505957 29.6574606,40.6935957 27.2774606,40.1025957 L29.2054606,32.3735957 C31.5854606,32.9675957 39.2174606,34.1435957 38.0834606,38.6905957 Z M39.1694606,27.3785957 C38.1794606,31.3445957 32.0694606,29.3295957 30.0874606,28.8355957 L31.8354606,21.8255957 C33.8174606,22.3195957 40.2004606,23.2415957 39.1694606,27.3785957 Z') + bitcoinPath.setAttribute('fill', '#f7931a') // Orange color for Bitcoin + bitcoinPath.setAttribute('transform', `translate(${x - 45}, ${y - 50}) scale(1.4)`) // Scale and position the logo + + logoGroup.appendChild(bitcoinPath) + svg.appendChild(logoGroup) + + // Add "Incoming Payment" text above the Bitcoin logo + const incomingText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + incomingText.setAttribute('x', x) + incomingText.setAttribute('y', y - 50) + incomingText.setAttribute('text-anchor', 'middle') + incomingText.setAttribute('fill', '#f7931a') + incomingText.setAttribute('font-family', 'Arial, sans-serif') + incomingText.setAttribute('font-size', '16px') + incomingText.setAttribute('font-weight', 'bold') + incomingText.textContent = 'Incoming Payment' + + svg.appendChild(incomingText) + }, + addPercentageLabel(svg, x, y, percentage, color) { // Create background circle for percentage const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index a8a29eb..d04972f 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -179,7 +179,7 @@
- +
Split Recipient {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ targets.length }}{% endraw %} Remove this recipient @@ -356,7 +357,7 @@
-
Split Payment Summary
+
Split Payment Summary
Important:
- • Splits totaling 100% may fail due to Lightning routing fees. Please keep this in mind when setting up splits for a wallet where small payments are common. - • Each payment to this wallet will be automatically split when it arrives +

• Splits totaling 100% may fail due to Lightning routing fees. Please keep this in mind when setting up splits for a wallet where small payments are common. +
+ • Each payment to this wallet will be automatically split when it arrives

@@ -855,7 +857,6 @@
.summary-card-title, .summary-card-main-text, .summary-card-sub-text, -.section-title, .target-name, .target-wallet { font-family: Roboto, "-apple-system", "Helvetica Neue", Helvetica, Arial, sans-serif; @@ -913,13 +914,6 @@
margin-bottom: 24px; } -.section-title { - font-size: 1.125rem; - font-weight: 600; - color: #374151; - margin-bottom: 16px; -} - .targets-list { display: flex; flex-direction: column; @@ -1010,11 +1004,6 @@
font-size: 0.75rem; } - .section-title { - font-size: 1rem; - margin-bottom: 12px; - } - .targets-list { gap: 8px; } From ab77716f9d57f1d51eca493c5e96183cbb1094d8 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:20:19 +0100 Subject: [PATCH 21/35] Hide chart on mobile as it will rarely fit once more than one payment is split --- templates/splitpayments/index.html | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index d04972f..201e3de 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -275,8 +275,8 @@
-
Split Preview
+
Split Preview
@@ -381,8 +381,8 @@
-
Payment Flow Preview
+
Payment Flow Preview
@@ -578,12 +578,7 @@
@media (max-width: 600px) { .flow-chart-container { - padding: 10px; - min-height: 300px; - } - - .flow-chart { - min-height: 300px; + display: none; } } From 3ebfbdf189e9e5b1435276ecbbf0acb2e89949e5 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Mon, 14 Jul 2025 19:21:32 +0100 Subject: [PATCH 22/35] Change remove button label --- templates/splitpayments/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 201e3de..10ba9b4 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -198,7 +198,7 @@ text-color="white" negative class="remove-btn" - label="Remove this recipient" + label="Remove" > Remove this recipient From ccfe0205d38d6a8bd130e246c59bdd5524ffcec7 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 09:40:06 +0100 Subject: [PATCH 23/35] Moved chart into standalone file --- static/image/bitcoin-logo.svg | 9 + static/js/chart.js | 398 ++++++++++++++++++++++++ static/js/index.js | 483 +---------------------------- templates/splitpayments/index.html | 17 +- 4 files changed, 423 insertions(+), 484 deletions(-) create mode 100644 static/image/bitcoin-logo.svg create mode 100644 static/js/chart.js diff --git a/static/image/bitcoin-logo.svg b/static/image/bitcoin-logo.svg new file mode 100644 index 0000000..c23cb37 --- /dev/null +++ b/static/image/bitcoin-logo.svg @@ -0,0 +1,9 @@ + + + Shape + + + + \ No newline at end of file diff --git a/static/js/chart.js b/static/js/chart.js new file mode 100644 index 0000000..b1b2a35 --- /dev/null +++ b/static/js/chart.js @@ -0,0 +1,398 @@ +// Split Payments Flow Chart Component +window.SplitPaymentsChart = Vue.defineComponent({ + name: 'SplitPaymentsChart', + props: { + splitDiagramData: { + type: Array, + required: true + }, + selectedWallet: { + type: Object, + default: null + }, + remainingPercent: { + type: Number, + default: 0 + } + }, + mounted() { + this.createFlowChart() + }, + watch: { + splitDiagramData: { + handler() { + this.$nextTick(() => { + this.createFlowChart() + }) + }, + deep: true + } + }, + methods: { + createFlowChart() { + try { + const container = this.$refs.chartContainer + if (!container) { + console.warn('Chart container not found') + return + } + + // Clear previous content + container.innerHTML = '' + + // Create SVG element + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + svg.setAttribute('width', '100%') + svg.setAttribute('height', '400') + svg.setAttribute('viewBox', '0 0 400 400') + svg.style.background = 'transparent' + + // Get targets data and source data + const targets = this.splitDiagramData.filter(item => item.type === 'target') + const sourceRemaining = this.splitDiagramData.filter(item => item.type === 'source') + + if (targets.length === 0) { + container.appendChild(svg) + return + } + + // Define positions + const sourceX = 200 + const sourceY = 80 + const targetY = 320 + + // Calculate bottom row items (targets + source if remaining > 0) + const bottomRowItems = [...targets] + if (sourceRemaining.length > 0 && this.remainingPercent > 0) { + bottomRowItems.push({ + name: this.selectedWallet ? this.selectedWallet.name : 'Source', + percent: this.remainingPercent, + type: 'source_remaining', + color: '#96A6FF' + }) + } + + // Calculate positions for bottom row items + const bottomRowPositions = [] + if (bottomRowItems.length === 1) { + bottomRowPositions.push({ x: sourceX, y: targetY }) + } else { + // Use the full width of the SVG viewBox (400px) with padding + const padding = 10 // Padding from edges + const totalWidth = 400 - (padding * 2) // Available width + const spacing = totalWidth / (bottomRowItems.length - 1) + const startX = padding + + bottomRowItems.forEach((item, index) => { + bottomRowPositions.push({ x: startX + (index * spacing), y: targetY }) + }) + } + + // Calculate proportional line thickness + const maxPercent = Math.max(...bottomRowItems.map(t => t.percent)) + const maxThickness = 30 // Maximum line thickness in pixels + + // Draw flowing lines - source_remaining lines first (behind other lines) + // First pass: draw source_remaining lines + bottomRowItems.forEach((item, index) => { + if (item.type === 'source_remaining') { + const itemPos = bottomRowPositions[index] + // Calculate thickness proportional to the highest percentage + const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) + + // End the line before the wallet icon (30px is wallet icon radius) + const lineEndY = targetY - 45 + this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') + } + }) + + // Second pass: draw target lines (on top of source_remaining lines) + bottomRowItems.forEach((item, index) => { + if (item.type === 'target') { + const itemPos = bottomRowPositions[index] + // Calculate thickness proportional to the highest percentage + const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) + + // End the line before the wallet icon (30px is wallet icon radius) + const lineEndY = targetY - 45 + this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') + } + }) + + // Draw source Bitcoin logo + this.drawBitcoinLogo(svg, sourceX, sourceY) + + // Draw bottom row wallet icons + bottomRowItems.forEach((item, index) => { + const itemPos = bottomRowPositions[index] + if (item.type === 'source_remaining') { + this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'source_remaining', item.percent, item.name) + } else { + this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'target', item.percent, item.name) + } + }) + + container.appendChild(svg) + console.log('Flow chart created successfully') + } catch (error) { + console.error('Error creating flow chart:', error) + } + }, + + drawFlowingLine(svg, x1, y1, x2, y2, finalThickness, color) { + // Create a tapered line that starts at 10px and increases to finalThickness + const startThickness = 10 + const segments = 20 // Number of segments for smooth taper + const midY = y1 + (y2 - y1) * 0.6 + + // Generate points along the quadratic Bezier curve + const points = [] + for (let i = 0; i <= segments; i++) { + const t = i / segments + let x, y + + if (t <= 0.5) { + // First quadratic curve: (x1, y1) to ((x1+x2)/2, midY) + const localT = t * 2 + const p0 = {x: x1, y: y1} + const p1 = {x: x1, y: midY} + const p2 = {x: (x1 + x2) / 2, y: midY} + + x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x + y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y + } else { + // Second quadratic curve: ((x1+x2)/2, midY) to (x2, y2) + const localT = (t - 0.5) * 2 + const p0 = {x: (x1 + x2) / 2, y: midY} + const p1 = {x: x2, y: midY} + const p2 = {x: x2, y: y2} + + x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x + y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y + } + + // Calculate thickness at this point + const thickness = startThickness + (finalThickness - startThickness) * t + points.push({x, y, thickness}) + } + + // Create polygon points for the tapered line + const leftPoints = [] + const rightPoints = [] + + for (let i = 0; i < points.length; i++) { + const point = points[i] + const halfThickness = point.thickness / 2 + + // Calculate direction vector + let dx = 0, dy = 1 + if (i < points.length - 1) { + dx = points[i + 1].x - point.x + dy = points[i + 1].y - point.y + } else if (i > 0) { + dx = point.x - points[i - 1].x + dy = point.y - points[i - 1].y + } + + // Normalize direction vector + const length = Math.sqrt(dx * dx + dy * dy) + if (length > 0) { + dx /= length + dy /= length + } + + // Calculate perpendicular offset (rotate 90 degrees) + const perpX = -dy + const perpY = dx + + // Add points to left and right sides + leftPoints.push({ + x: point.x + perpX * halfThickness, + y: point.y + perpY * halfThickness + }) + rightPoints.unshift({ + x: point.x - perpX * halfThickness, + y: point.y - perpY * halfThickness + }) + } + + // Create arrow tip pointing down + const lastPoint = points[points.length - 1] + const arrowHeight = 15 // Fixed arrow height so all arrows terminate at same Y position + + // Get the last left and right points to connect seamlessly + const lastLeftPoint = leftPoints[leftPoints.length - 1] + const lastRightPoint = rightPoints[0] // rightPoints is reversed, so first element is the last point + + // Arrow tip points - connect directly to the line ends + const arrowTip = {x: lastPoint.x, y: lastPoint.y + arrowHeight} + + // Create combined polygon including line body and arrow + const allPoints = [ + ...leftPoints.slice(0, -1), // All left points except the last one + lastLeftPoint, // Last left point + arrowTip, // Arrow tip + lastRightPoint, // Last right point + ...rightPoints.slice(1) // All right points except the first one + ] + + const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon') + const pointsString = allPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') + + polygon.setAttribute('points', pointsString) + polygon.setAttribute('fill', color) + polygon.setAttribute('opacity', '1') + + svg.appendChild(polygon) + }, + + drawWalletIcon(svg, x, y, type, percentage, targetName = null) { + // Create wallet icon using the PNG image + const image = document.createElementNS('http://www.w3.org/2000/svg', 'image') + image.setAttribute('x', x - 30) + image.setAttribute('y', y - 30) + image.setAttribute('width', 60) + image.setAttribute('height', 60) + + image.setAttribute('href', '/splitpayments/static/image/icon-wallet.png') + + // Add color filter for source vs target distinction + if (type === 'source' || type === 'source_remaining') { + // Add blue tint for source wallet + image.setAttribute('style', 'filter: hue-rotate(200deg) saturate(1.2)') + } + + // Add error handling - if image fails to load, show a fallback + image.addEventListener('error', () => { + console.warn('Failed to load wallet icon, using fallback') + // Remove the broken image and replace with a styled rectangle + svg.removeChild(image) + this.drawFallbackWalletIcon(svg, x, y, type, percentage, targetName) + }) + + svg.appendChild(image) + + // Add name and percentage below icon for targets and source_remaining + if (type === 'target' || type === 'source_remaining') { + // Add name text + if (targetName) { + const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + nameText.setAttribute('x', x) + nameText.setAttribute('y', y + 45) + nameText.setAttribute('text-anchor', 'middle') + nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') + nameText.setAttribute('font-family', 'Arial, sans-serif') + nameText.setAttribute('font-size', '14px') + nameText.setAttribute('font-weight', 'bold') + nameText.textContent = targetName + + svg.appendChild(nameText) + } + + // Add percentage text below name + const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + percentText.setAttribute('x', x) + percentText.setAttribute('y', y + 80) + percentText.setAttribute('text-anchor', 'middle') + percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') + percentText.setAttribute('font-family', 'Arial, sans-serif') + percentText.setAttribute('font-size', '32px') + percentText.setAttribute('font-weight', 'bold') + percentText.textContent = `${percentage}%` + + svg.appendChild(percentText) + } + }, + + drawFallbackWalletIcon(svg, x, y, type, percentage, targetName = null) { + // Fallback wallet icon when PNG fails to load + const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') + rect.setAttribute('x', x - 30) + rect.setAttribute('y', y - 30) + rect.setAttribute('width', 60) + rect.setAttribute('height', 60) + rect.setAttribute('rx', 12) + rect.setAttribute('fill', (type === 'source' || type === 'source_remaining') ? (type === 'source_remaining' ? '#96A6FF' : '#6366f1') : '#f59e0b') + rect.setAttribute('stroke', '#1f2937') + rect.setAttribute('stroke-width', 2) + + svg.appendChild(rect) + + // Add Bitcoin symbol + const text = document.createElementNS('http://www.w3.org/2000/svg', 'text') + text.setAttribute('x', x) + text.setAttribute('y', y + 5) + text.setAttribute('text-anchor', 'middle') + text.setAttribute('fill', 'white') + text.setAttribute('font-family', 'Arial, sans-serif') + text.setAttribute('font-size', '24') + text.setAttribute('font-weight', 'bold') + text.textContent = '₿' + + svg.appendChild(text) + + // Add name and percentage below icon for targets and source_remaining + if (type === 'target' || type === 'source_remaining') { + // Add name text + if (targetName) { + const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + nameText.setAttribute('x', x) + nameText.setAttribute('y', y + 45) + nameText.setAttribute('text-anchor', 'middle') + nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') + nameText.setAttribute('font-family', 'Arial, sans-serif') + nameText.setAttribute('font-size', '14px') + nameText.setAttribute('font-weight', 'bold') + nameText.textContent = targetName + + svg.appendChild(nameText) + } + + // Add percentage text below name + const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + percentText.setAttribute('x', x) + percentText.setAttribute('y', y + 65) + percentText.setAttribute('text-anchor', 'middle') + percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') + percentText.setAttribute('font-family', 'Arial, sans-serif') + percentText.setAttribute('font-size', '16px') + percentText.setAttribute('font-weight', 'bold') + percentText.textContent = `${percentage}%` + + svg.appendChild(percentText) + } + }, + + drawBitcoinLogo(svg, x, y) { + // Create Bitcoin logo using SVG + const logoGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g') + + const bitcoinPath = document.createElementNS('http://www.w3.org/2000/svg', 'path') + bitcoinPath.setAttribute('d', 'M39.0674606,19.3675957 L40.5054606,13.5995957 L36.9944606,12.7245957 L35.5944606,18.3405957 C34.6714606,18.1105957 33.7234606,17.8935957 32.7814606,17.6785957 L34.1914606,12.0255957 L30.6824606,11.1505957 L29.2434606,16.9165957 C28.4794606,16.7425957 27.7294606,16.5705957 27.0014606,16.3895957 L27.0054606,16.3715957 L22.1634606,15.1625957 L21.2294606,18.9125957 C21.2294606,18.9125957 23.8344606,19.5095957 23.7794606,19.5465957 C25.2014606,19.9015957 25.4584606,20.8425957 25.4154606,21.5885957 L23.7774606,28.1595957 L23.7714606,28.1845957 L21.4754606,37.3895957 C21.3014606,37.8215957 20.8604606,38.4695957 19.8664606,38.2235957 C19.9014606,38.2745957 17.3144606,37.5865957 17.3144606,37.5865957 L15.5714606,41.6055957 L20.1404606,42.7445957 C20.9904606,42.9575957 21.8234606,43.1805957 22.6434606,43.3905957 L21.1904606,49.2245957 L24.6974606,50.0995957 L26.1364606,44.3275957 C27.0944606,44.5875957 28.0244606,44.8275957 28.9344606,45.0535957 L27.5004606,50.7985957 L31.0114606,51.6735957 L32.4644606,45.8505957 C38.4514606,46.9835957 42.9534606,46.5265957 44.8484606,41.1115957 C46.3754606,36.7515957 44.7724606,34.2365957 41.6224606,32.5965957 C43.9164606,32.0675957 45.6444606,30.5585957 46.1054606,27.4415957 C46.7424606,23.1835957 43.5004606,20.8945957 39.0674606,19.3675957 Z M38.0834606,38.6905957 C36.9984606,43.0505957 29.6574606,40.6935957 27.2774606,40.1025957 L29.2054606,32.3735957 C31.5854606,32.9675957 39.2174606,34.1435957 38.0834606,38.6905957 Z M39.1694606,27.3785957 C38.1794606,31.3445957 32.0694606,29.3295957 30.0874606,28.8355957 L31.8354606,21.8255957 C33.8174606,22.3195957 40.2004606,23.2415957 39.1694606,27.3785957 Z') + bitcoinPath.setAttribute('fill', '#f7931a') // Orange color for Bitcoin + bitcoinPath.setAttribute('transform', `translate(${x - 45}, ${y - 50}) scale(1.4)`) // Scale and position the logo + + logoGroup.appendChild(bitcoinPath) + svg.appendChild(logoGroup) + + // Add "Incoming Payment" text above the Bitcoin logo + const incomingText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + incomingText.setAttribute('x', x) + incomingText.setAttribute('y', y - 50) + incomingText.setAttribute('text-anchor', 'middle') + incomingText.setAttribute('fill', '#f7931a') + incomingText.setAttribute('font-family', 'Arial, sans-serif') + incomingText.setAttribute('font-size', '16px') + incomingText.setAttribute('font-weight', 'bold') + incomingText.textContent = 'Incoming Payment' + + svg.appendChild(incomingText) + } + }, + + template: ` +
+
+
+ ` +}) \ No newline at end of file diff --git a/static/js/index.js b/static/js/index.js index e793b9f..9b0bccb 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -16,21 +16,13 @@ function isTargetComplete(target) { window.app = Vue.createApp({ el: '#vue', mixins: [windowMixin], + components: { + 'split-payments-chart': SplitPaymentsChart + }, watch: { selectedWallet() { this.getTargets() }, - splitDiagramData() { - // Recreate flow charts when data changes - this.$nextTick(() => { - this.recreateCharts() - }) - }, - currentStep() { - this.$nextTick(() => { - this.initFlowChart() - }) - } }, data() { return { @@ -41,12 +33,7 @@ window.app = Vue.createApp({ // Existing data selectedWallet: null, currentHash: '', // a string that must match if the edit data is unchanged - targets: [], - - // Chart instances - treeChart: null, - treeChart2: null, - chartUpdateTimeout: null + targets: [] } }, computed: { @@ -213,455 +200,6 @@ window.app = Vue.createApp({ ) }, - // SVG Flow Chart methods - initFlowChart() { - - // Create chart for Step 2 - if (this.$refs.flowChart && this.currentStep === 2) { - this.createFlowChart('flowChart') - } - - // Create chart for Step 3 - if (this.$refs.flowChart2 && this.currentStep === 3) { - this.createFlowChart('flowChart2') - } - }, - recreateCharts() { - // Safely recreate charts when data changes - if (this.currentStep === 2 && this.splitDiagramData.length > 0) { - this.$nextTick(() => { - this.createFlowChart('flowChart') - }) - } - if (this.currentStep === 3 && this.splitDiagramData.length > 0) { - this.$nextTick(() => { - this.createFlowChart('flowChart2') - }) - } - }, - createFlowChart(containerRef) { - try { - if (!this.$refs[containerRef]) { - console.warn('Container ref not found:', containerRef) - return - } - - const container = this.$refs[containerRef] - - // Clear previous content - container.innerHTML = '' - - // Create SVG element - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') - svg.setAttribute('width', '100%') - svg.setAttribute('height', '400') - svg.setAttribute('viewBox', '0 0 400 400') - svg.style.background = 'transparent' - - // Get targets data and source data - const targets = this.splitDiagramData.filter(item => item.type === 'target') - const sourceRemaining = this.splitDiagramData.filter(item => item.type === 'source') - - if (targets.length === 0) { - container.appendChild(svg) - return - } - - // Define positions - const sourceX = 200 - const sourceY = 80 - const branchY = 200 - const targetY = 320 - - // Calculate bottom row items (targets + source if remaining > 0) - const bottomRowItems = [...targets] - if (sourceRemaining.length > 0 && this.remainingPercent > 0) { - bottomRowItems.push({ - name: this.selectedWallet ? this.selectedWallet.name : 'Source', - percent: this.remainingPercent, - type: 'source_remaining', - color: '#96A6FF' - }) - } - - // Calculate positions for bottom row items - const bottomRowPositions = [] - if (bottomRowItems.length === 1) { - bottomRowPositions.push({ x: sourceX, y: targetY }) - } else { - // Use the full width of the SVG viewBox (400px) with padding - const padding = 10 // Padding from edges - const totalWidth = 400 - (padding * 2) // Available width - const spacing = totalWidth / (bottomRowItems.length - 1) - const startX = padding - - bottomRowItems.forEach((item, index) => { - bottomRowPositions.push({ x: startX + (index * spacing), y: targetY }) - }) - } - - // Calculate proportional line thickness - const maxPercent = Math.max(...bottomRowItems.map(t => t.percent)) - const maxThickness = 30 // Maximum line thickness in pixels - - // Draw flowing lines - source_remaining lines first (behind other lines) - // First pass: draw source_remaining lines - bottomRowItems.forEach((item, index) => { - if (item.type === 'source_remaining') { - const itemPos = bottomRowPositions[index] - // Calculate thickness proportional to the highest percentage - const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - - // End the line before the wallet icon (30px is wallet icon radius) - const lineEndY = targetY - 45 - this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') - } - }) - - // Second pass: draw target lines (on top of source_remaining lines) - bottomRowItems.forEach((item, index) => { - if (item.type === 'target') { - const itemPos = bottomRowPositions[index] - // Calculate thickness proportional to the highest percentage - const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - - // End the line before the wallet icon (30px is wallet icon radius) - const lineEndY = targetY - 45 - this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') - } - }) - - // Draw source Bitcoin logo - this.drawBitcoinLogo(svg, sourceX, sourceY) - - // Draw bottom row wallet icons - bottomRowItems.forEach((item, index) => { - const itemPos = bottomRowPositions[index] - if (item.type === 'source_remaining') { - this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'source_remaining', item.percent, item.name) - } else { - this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'target', item.percent, item.name) - } - }) - - container.appendChild(svg) - } catch (error) { - console.error('Error creating flow chart:', error) - } - }, - drawFlowingLine(svg, x1, y1, x2, y2, finalThickness, color) { - // Create a tapered line that starts at 10px and increases to finalThickness - const startThickness = 10 - const segments = 20 // Number of segments for smooth taper - const midY = y1 + (y2 - y1) * 0.6 - - // Generate points along the quadratic Bezier curve - const points = [] - for (let i = 0; i <= segments; i++) { - const t = i / segments - let x, y - - if (t <= 0.5) { - // First quadratic curve: (x1, y1) to ((x1+x2)/2, midY) - const localT = t * 2 - const p0 = {x: x1, y: y1} - const p1 = {x: x1, y: midY} - const p2 = {x: (x1 + x2) / 2, y: midY} - - x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x - y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y - } else { - // Second quadratic curve: ((x1+x2)/2, midY) to (x2, y2) - const localT = (t - 0.5) * 2 - const p0 = {x: (x1 + x2) / 2, y: midY} - const p1 = {x: x2, y: midY} - const p2 = {x: x2, y: y2} - - x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x - y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y - } - - // Calculate thickness at this point - const thickness = startThickness + (finalThickness - startThickness) * t - points.push({x, y, thickness}) - } - - // Create polygon points for the tapered line - const leftPoints = [] - const rightPoints = [] - - for (let i = 0; i < points.length; i++) { - const point = points[i] - const halfThickness = point.thickness / 2 - - // Calculate direction vector - let dx = 0, dy = 1 - if (i < points.length - 1) { - dx = points[i + 1].x - point.x - dy = points[i + 1].y - point.y - } else if (i > 0) { - dx = point.x - points[i - 1].x - dy = point.y - points[i - 1].y - } - - // Normalize direction vector - const length = Math.sqrt(dx * dx + dy * dy) - if (length > 0) { - dx /= length - dy /= length - } - - // Calculate perpendicular offset (rotate 90 degrees) - const perpX = -dy - const perpY = dx - - // Add points to left and right sides - leftPoints.push({ - x: point.x + perpX * halfThickness, - y: point.y + perpY * halfThickness - }) - rightPoints.unshift({ - x: point.x - perpX * halfThickness, - y: point.y - perpY * halfThickness - }) - } - - // Create arrow tip pointing down - const lastPoint = points[points.length - 1] - const arrowHeight = 15 // Fixed arrow height so all arrows terminate at same Y position - - // Get the last left and right points to connect seamlessly - const lastLeftPoint = leftPoints[leftPoints.length - 1] - const lastRightPoint = rightPoints[0] // rightPoints is reversed, so first element is the last point - - // Arrow tip points - connect directly to the line ends - const arrowTip = {x: lastPoint.x, y: lastPoint.y + arrowHeight} - - // Create combined polygon including line body and arrow - const allPoints = [ - ...leftPoints.slice(0, -1), // All left points except the last one - lastLeftPoint, // Last left point - arrowTip, // Arrow tip - lastRightPoint, // Last right point - ...rightPoints.slice(1) // All right points except the first one - ] - - const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon') - const pointsString = allPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') - - polygon.setAttribute('points', pointsString) - polygon.setAttribute('fill', color) - polygon.setAttribute('opacity', '1') - - svg.appendChild(polygon) - }, - - drawWalletIcon(svg, x, y, type, percentage, targetName = null) { - // Create wallet icon using the PNG image - const image = document.createElementNS('http://www.w3.org/2000/svg', 'image') - image.setAttribute('x', x - 30) - image.setAttribute('y', y - 30) - image.setAttribute('width', 60) - image.setAttribute('height', 60) - - // Try different possible paths for the wallet icon - const possiblePaths = [ - '/splitpayments/static/image/icon-wallet.png', - '/static/image/icon-wallet.png', - 'static/image/icon-wallet.png' - ] - - image.setAttribute('href', possiblePaths[0]) - - // Add color filter for source vs target distinction - if (type === 'source' || type === 'source_remaining') { - // Add blue tint for source wallet - image.setAttribute('style', 'filter: hue-rotate(200deg) saturate(1.2)') - } - - // Add error handling - if image fails to load, show a fallback - image.addEventListener('error', () => { - console.warn('Failed to load wallet icon, using fallback') - // Remove the broken image and replace with a styled rectangle - svg.removeChild(image) - this.drawFallbackWalletIcon(svg, x, y, type, percentage, targetName) - }) - - svg.appendChild(image) - - // Add source wallet name above icon if it's a source - if (type === 'source') { - // Add source wallet name text above the icon - const sourceNameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - sourceNameText.setAttribute('x', x) - sourceNameText.setAttribute('y', y - 45) - sourceNameText.setAttribute('text-anchor', 'middle') - sourceNameText.setAttribute('fill', '#1976d2') - sourceNameText.setAttribute('font-family', 'Arial, sans-serif') - sourceNameText.setAttribute('font-size', '14px') - sourceNameText.setAttribute('font-weight', 'bold') - sourceNameText.textContent = this.selectedWallet ? this.selectedWallet.name : 'Source Wallet' - - svg.appendChild(sourceNameText) - } - - // Add name and percentage below icon for targets and source_remaining - if (type === 'target' || type === 'source_remaining') { - // Add name text - if (targetName) { - const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - nameText.setAttribute('x', x) - nameText.setAttribute('y', y + 45) - nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') - nameText.setAttribute('font-family', 'Arial, sans-serif') - nameText.setAttribute('font-size', '14px') - nameText.setAttribute('font-weight', 'bold') - nameText.textContent = targetName - - svg.appendChild(nameText) - } - - // Add percentage text below name - const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - percentText.setAttribute('x', x) - percentText.setAttribute('y', y + 80) - percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') - percentText.setAttribute('font-family', 'Arial, sans-serif') - percentText.setAttribute('font-size', '32px') - percentText.setAttribute('font-weight', 'bold') - percentText.textContent = `${percentage}%` - - svg.appendChild(percentText) - } - }, - - drawFallbackWalletIcon(svg, x, y, type, percentage, targetName = null) { - // Fallback wallet icon when PNG fails to load - const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') - rect.setAttribute('x', x - 30) - rect.setAttribute('y', y - 30) - rect.setAttribute('width', 60) - rect.setAttribute('height', 60) - rect.setAttribute('rx', 12) - rect.setAttribute('fill', (type === 'source' || type === 'source_remaining') ? (type === 'source_remaining' ? '#96A6FF' : '#6366f1') : '#f59e0b') - rect.setAttribute('stroke', '#1f2937') - rect.setAttribute('stroke-width', 2) - - svg.appendChild(rect) - - // Add Bitcoin symbol - const text = document.createElementNS('http://www.w3.org/2000/svg', 'text') - text.setAttribute('x', x) - text.setAttribute('y', y + 5) - text.setAttribute('text-anchor', 'middle') - text.setAttribute('fill', 'white') - text.setAttribute('font-family', 'Arial, sans-serif') - text.setAttribute('font-size', '24') - text.setAttribute('font-weight', 'bold') - text.textContent = '₿' - - svg.appendChild(text) - - // Add source wallet name above icon if it's a source - if (type === 'source') { - // Add source wallet name text above the icon - const sourceNameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - sourceNameText.setAttribute('x', x) - sourceNameText.setAttribute('y', y - 45) - sourceNameText.setAttribute('text-anchor', 'middle') - sourceNameText.setAttribute('fill', '#1976d2') - sourceNameText.setAttribute('font-family', 'Arial, sans-serif') - sourceNameText.setAttribute('font-size', '14px') - sourceNameText.setAttribute('font-weight', 'bold') - sourceNameText.textContent = this.selectedWallet ? this.selectedWallet.name : 'Source Wallet' - - svg.appendChild(sourceNameText) - } - - // Add name and percentage below icon for targets and source_remaining - if (type === 'target' || type === 'source_remaining') { - // Add name text - if (targetName) { - const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - nameText.setAttribute('x', x) - nameText.setAttribute('y', y + 45) - nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') - nameText.setAttribute('font-family', 'Arial, sans-serif') - nameText.setAttribute('font-size', '14px') - nameText.setAttribute('font-weight', 'bold') - nameText.textContent = targetName - - svg.appendChild(nameText) - } - - // Add percentage text below name - const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - percentText.setAttribute('x', x) - percentText.setAttribute('y', y + 65) - percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') - percentText.setAttribute('font-family', 'Arial, sans-serif') - percentText.setAttribute('font-size', '16px') - percentText.setAttribute('font-weight', 'bold') - percentText.textContent = `${percentage}%` - - svg.appendChild(percentText) - } - }, - - drawBitcoinLogo(svg, x, y) { - // Create Bitcoin logo using SVG - const logoGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g') - - const bitcoinPath = document.createElementNS('http://www.w3.org/2000/svg', 'path') - bitcoinPath.setAttribute('d', 'M39.0674606,19.3675957 L40.5054606,13.5995957 L36.9944606,12.7245957 L35.5944606,18.3405957 C34.6714606,18.1105957 33.7234606,17.8935957 32.7814606,17.6785957 L34.1914606,12.0255957 L30.6824606,11.1505957 L29.2434606,16.9165957 C28.4794606,16.7425957 27.7294606,16.5705957 27.0014606,16.3895957 L27.0054606,16.3715957 L22.1634606,15.1625957 L21.2294606,18.9125957 C21.2294606,18.9125957 23.8344606,19.5095957 23.7794606,19.5465957 C25.2014606,19.9015957 25.4584606,20.8425957 25.4154606,21.5885957 L23.7774606,28.1595957 L23.7714606,28.1845957 L21.4754606,37.3895957 C21.3014606,37.8215957 20.8604606,38.4695957 19.8664606,38.2235957 C19.9014606,38.2745957 17.3144606,37.5865957 17.3144606,37.5865957 L15.5714606,41.6055957 L20.1404606,42.7445957 C20.9904606,42.9575957 21.8234606,43.1805957 22.6434606,43.3905957 L21.1904606,49.2245957 L24.6974606,50.0995957 L26.1364606,44.3275957 C27.0944606,44.5875957 28.0244606,44.8275957 28.9344606,45.0535957 L27.5004606,50.7985957 L31.0114606,51.6735957 L32.4644606,45.8505957 C38.4514606,46.9835957 42.9534606,46.5265957 44.8484606,41.1115957 C46.3754606,36.7515957 44.7724606,34.2365957 41.6224606,32.5965957 C43.9164606,32.0675957 45.6444606,30.5585957 46.1054606,27.4415957 C46.7424606,23.1835957 43.5004606,20.8945957 39.0674606,19.3675957 Z M38.0834606,38.6905957 C36.9984606,43.0505957 29.6574606,40.6935957 27.2774606,40.1025957 L29.2054606,32.3735957 C31.5854606,32.9675957 39.2174606,34.1435957 38.0834606,38.6905957 Z M39.1694606,27.3785957 C38.1794606,31.3445957 32.0694606,29.3295957 30.0874606,28.8355957 L31.8354606,21.8255957 C33.8174606,22.3195957 40.2004606,23.2415957 39.1694606,27.3785957 Z') - bitcoinPath.setAttribute('fill', '#f7931a') // Orange color for Bitcoin - bitcoinPath.setAttribute('transform', `translate(${x - 45}, ${y - 50}) scale(1.4)`) // Scale and position the logo - - logoGroup.appendChild(bitcoinPath) - svg.appendChild(logoGroup) - - // Add "Incoming Payment" text above the Bitcoin logo - const incomingText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - incomingText.setAttribute('x', x) - incomingText.setAttribute('y', y - 50) - incomingText.setAttribute('text-anchor', 'middle') - incomingText.setAttribute('fill', '#f7931a') - incomingText.setAttribute('font-family', 'Arial, sans-serif') - incomingText.setAttribute('font-size', '16px') - incomingText.setAttribute('font-weight', 'bold') - incomingText.textContent = 'Incoming Payment' - - svg.appendChild(incomingText) - }, - - addPercentageLabel(svg, x, y, percentage, color) { - // Create background circle for percentage - const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle') - circle.setAttribute('cx', x) - circle.setAttribute('cy', y) - circle.setAttribute('r', 20) - circle.setAttribute('fill', color) - circle.setAttribute('opacity', '1') - - svg.appendChild(circle) - - // Add percentage text - const text = document.createElementNS('http://www.w3.org/2000/svg', 'text') - text.setAttribute('x', x) - text.setAttribute('y', y + 6) - text.setAttribute('text-anchor', 'middle') - text.setAttribute('fill', 'white') - text.setAttribute('font-family', 'Arial, sans-serif') - text.setAttribute('font-size', '20px') - text.setAttribute('font-weight', 'bold') - text.textContent = percentage - - svg.appendChild(text) - }, // Target management methods clearTarget(index) { @@ -817,17 +355,6 @@ window.app = Vue.createApp({ } }, mounted() { - this.$nextTick(() => { - this.initFlowChart() - }) + this.checkExistingConfigurations() }, - beforeUnmount() { - // Clean up flow chart containers - if (this.$refs.flowChart) { - this.$refs.flowChart.innerHTML = '' - } - if (this.$refs.flowChart2) { - this.$refs.flowChart2.innerHTML = '' - } - } }) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 10ba9b4..47b29c8 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -275,10 +275,12 @@
-
Split Preview
-
-
+
@@ -381,10 +383,12 @@
-
Payment Flow Preview
-
-
+
@@ -1062,5 +1066,6 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }} + {% endblock %} \ No newline at end of file From ee3a04dd1ca2f5d1f36db214935aa42b44efd1cf Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 09:41:14 +0100 Subject: [PATCH 24/35] Revert package.json version to 1.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 336801a..8af073b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitpayments", - "version": "1.0.2", + "version": "1.0.0", "description": "", "main": "index.js", "scripts": { From a703f21e882c809e4bacd84cf35af8eb24720241 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:38:15 +0100 Subject: [PATCH 25/35] Respect dark theme --- static/js/chart.js | 23 ++++---- templates/splitpayments/index.html | 92 +++++++++++++++++++++++++----- 2 files changed, 90 insertions(+), 25 deletions(-) diff --git a/static/js/chart.js b/static/js/chart.js index b1b2a35..a4703fe 100644 --- a/static/js/chart.js +++ b/static/js/chart.js @@ -40,6 +40,9 @@ window.SplitPaymentsChart = Vue.defineComponent({ // Clear previous content container.innerHTML = '' + // Detect dark theme + const isDarkTheme = document.body.classList.contains('body--dark') + // Create SVG element const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') svg.setAttribute('width', '100%') @@ -120,15 +123,15 @@ window.SplitPaymentsChart = Vue.defineComponent({ }) // Draw source Bitcoin logo - this.drawBitcoinLogo(svg, sourceX, sourceY) + this.drawBitcoinLogo(svg, sourceX, sourceY, isDarkTheme) // Draw bottom row wallet icons bottomRowItems.forEach((item, index) => { const itemPos = bottomRowPositions[index] if (item.type === 'source_remaining') { - this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'source_remaining', item.percent, item.name) + this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'source_remaining', item.percent, item.name, isDarkTheme) } else { - this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'target', item.percent, item.name) + this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'target', item.percent, item.name, isDarkTheme) } }) @@ -246,7 +249,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ svg.appendChild(polygon) }, - drawWalletIcon(svg, x, y, type, percentage, targetName = null) { + drawWalletIcon(svg, x, y, type, percentage, targetName = null, isDarkTheme = false) { // Create wallet icon using the PNG image const image = document.createElementNS('http://www.w3.org/2000/svg', 'image') image.setAttribute('x', x - 30) @@ -267,7 +270,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ console.warn('Failed to load wallet icon, using fallback') // Remove the broken image and replace with a styled rectangle svg.removeChild(image) - this.drawFallbackWalletIcon(svg, x, y, type, percentage, targetName) + this.drawFallbackWalletIcon(svg, x, y, type, percentage, targetName, isDarkTheme) }) svg.appendChild(image) @@ -280,7 +283,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ nameText.setAttribute('x', x) nameText.setAttribute('y', y + 45) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') + nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : (isDarkTheme ? '#f3f4f6' : '#374151')) nameText.setAttribute('font-family', 'Arial, sans-serif') nameText.setAttribute('font-size', '14px') nameText.setAttribute('font-weight', 'bold') @@ -304,7 +307,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ } }, - drawFallbackWalletIcon(svg, x, y, type, percentage, targetName = null) { + drawFallbackWalletIcon(svg, x, y, type, percentage, targetName = null, isDarkTheme = false) { // Fallback wallet icon when PNG fails to load const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') rect.setAttribute('x', x - 30) @@ -339,7 +342,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ nameText.setAttribute('x', x) nameText.setAttribute('y', y + 45) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#374151') + nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : (isDarkTheme ? '#f3f4f6' : '#374151')) nameText.setAttribute('font-family', 'Arial, sans-serif') nameText.setAttribute('font-size', '14px') nameText.setAttribute('font-weight', 'bold') @@ -363,7 +366,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ } }, - drawBitcoinLogo(svg, x, y) { + drawBitcoinLogo(svg, x, y, isDarkTheme = false) { // Create Bitcoin logo using SVG const logoGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g') @@ -380,7 +383,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ incomingText.setAttribute('x', x) incomingText.setAttribute('y', y - 50) incomingText.setAttribute('text-anchor', 'middle') - incomingText.setAttribute('fill', '#f7931a') + incomingText.setAttribute('fill', isDarkTheme ? '#f9ca24' : '#f7931a') incomingText.setAttribute('font-family', 'Arial, sans-serif') incomingText.setAttribute('font-size', '16px') incomingText.setAttribute('font-weight', 'bold') diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 47b29c8..65f4f84 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -321,7 +321,7 @@
- +
@@ -339,7 +339,7 @@ - +
@@ -567,12 +567,16 @@
justify-content: center; align-items: center; padding: 20px; - background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); + background: var(--q-color-grey-3); border-radius: 12px; margin: 16px 0; min-height: 400px; } +.body--dark .flow-chart-container { + background: var(--q-color-grey-9); +} + .flow-chart { width: 100%; height: 100%; @@ -866,6 +870,22 @@
overflow: hidden; } +.summary-card-source { + background: var(--q-color-blue-1); +} + +.summary-card-targets { + background: var(--q-color-green-1); +} + +.body--dark .summary-card-source { + background: var(--q-color-blue-10); +} + +.body--dark .summary-card-targets { + background: var(--q-color-green-10); +} + .summary-card-section { padding: 16px; } @@ -874,7 +894,23 @@
display: flex; align-items: center; margin-bottom: 12px; - color: #1976d2; + color: var(--q-color-primary); +} + +.summary-card-source .summary-card-header { + color: var(--q-color-blue-8); +} + +.summary-card-targets .summary-card-header { + color: var(--q-color-green-8); +} + +.body--dark .summary-card-source .summary-card-header { + color: var(--q-color-blue-4); +} + +.body--dark .summary-card-targets .summary-card-header { + color: var(--q-color-green-4); } .summary-card-title { @@ -893,21 +929,34 @@
.summary-card-main-text { font-size: 1.125rem; font-weight: 700; - color: #1976d2; + color: var(--q-color-primary); +} + +.summary-card-source .summary-card-main-text { + color: var(--q-color-blue-8); +} + +.summary-card-targets .summary-card-main-text { + color: var(--q-color-green-8); +} + +.body--dark .summary-card-source .summary-card-main-text { + color: var(--q-color-blue-4); +} + +.body--dark .summary-card-targets .summary-card-main-text { + color: var(--q-color-green-4); } .summary-card-sub-text { font-size: 0.875rem; - color: #6b7280; + color: var(--q-color-grey-7); } -.bg-green-1 .summary-card-header { - color: #059669; +.body--dark .summary-card-sub-text { + color: var(--q-color-grey-5); } -.bg-green-1 .summary-card-main-text { - color: #059669; -} .detailed-targets-container { margin-bottom: 24px; @@ -924,9 +973,14 @@
align-items: center; gap: 16px; padding: 16px; - background: #f8fafc; + background: var(--q-color-grey-1); border-radius: 8px; - border: 1px solid #e2e8f0; + border: 1px solid var(--q-color-grey-4); +} + +.body--dark .target-summary-item { + background: var(--q-color-grey-9); + border-color: var(--q-color-grey-7); } .target-percent-circle { @@ -953,15 +1007,23 @@
.target-name { font-size: 1rem; font-weight: 600; - color: #374151; + color: var(--q-color-grey-9); +} + +.body--dark .target-name { + color: var(--q-color-grey-3); } .target-wallet { font-size: 0.875rem; - color: #6b7280; + color: var(--q-color-grey-7); word-break: break-all; } +.body--dark .target-wallet { + color: var(--q-color-grey-5); +} + .step-3-navigation { justify-content: center; gap: 16px; From e3a64d475c1e099a36bf534faa29651fbeeab636 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:08:14 +0100 Subject: [PATCH 26/35] Chart component Replace css font specification with quasar fonts --- static/js/chart.js | 79 ++++------------------------------------------ 1 file changed, 6 insertions(+), 73 deletions(-) diff --git a/static/js/chart.js b/static/js/chart.js index a4703fe..b142f30 100644 --- a/static/js/chart.js +++ b/static/js/chart.js @@ -46,8 +46,8 @@ window.SplitPaymentsChart = Vue.defineComponent({ // Create SVG element const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') svg.setAttribute('width', '100%') - svg.setAttribute('height', '400') - svg.setAttribute('viewBox', '0 0 400 400') + svg.setAttribute('height', '500') + svg.setAttribute('viewBox', '0 0 400 450') svg.style.background = 'transparent' // Get targets data and source data @@ -270,7 +270,6 @@ window.SplitPaymentsChart = Vue.defineComponent({ console.warn('Failed to load wallet icon, using fallback') // Remove the broken image and replace with a styled rectangle svg.removeChild(image) - this.drawFallbackWalletIcon(svg, x, y, type, percentage, targetName, isDarkTheme) }) svg.appendChild(image) @@ -281,12 +280,10 @@ window.SplitPaymentsChart = Vue.defineComponent({ if (targetName) { const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') nameText.setAttribute('x', x) - nameText.setAttribute('y', y + 45) + nameText.setAttribute('y', y + 55) nameText.setAttribute('text-anchor', 'middle') nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : (isDarkTheme ? '#f3f4f6' : '#374151')) - nameText.setAttribute('font-family', 'Arial, sans-serif') - nameText.setAttribute('font-size', '14px') - nameText.setAttribute('font-weight', 'bold') + nameText.setAttribute('class', 'text-body2') nameText.textContent = targetName svg.appendChild(nameText) @@ -295,71 +292,10 @@ window.SplitPaymentsChart = Vue.defineComponent({ // Add percentage text below name const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') percentText.setAttribute('x', x) - percentText.setAttribute('y', y + 80) + percentText.setAttribute('y', y + 85) percentText.setAttribute('text-anchor', 'middle') percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') - percentText.setAttribute('font-family', 'Arial, sans-serif') - percentText.setAttribute('font-size', '32px') - percentText.setAttribute('font-weight', 'bold') - percentText.textContent = `${percentage}%` - - svg.appendChild(percentText) - } - }, - - drawFallbackWalletIcon(svg, x, y, type, percentage, targetName = null, isDarkTheme = false) { - // Fallback wallet icon when PNG fails to load - const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect') - rect.setAttribute('x', x - 30) - rect.setAttribute('y', y - 30) - rect.setAttribute('width', 60) - rect.setAttribute('height', 60) - rect.setAttribute('rx', 12) - rect.setAttribute('fill', (type === 'source' || type === 'source_remaining') ? (type === 'source_remaining' ? '#96A6FF' : '#6366f1') : '#f59e0b') - rect.setAttribute('stroke', '#1f2937') - rect.setAttribute('stroke-width', 2) - - svg.appendChild(rect) - - // Add Bitcoin symbol - const text = document.createElementNS('http://www.w3.org/2000/svg', 'text') - text.setAttribute('x', x) - text.setAttribute('y', y + 5) - text.setAttribute('text-anchor', 'middle') - text.setAttribute('fill', 'white') - text.setAttribute('font-family', 'Arial, sans-serif') - text.setAttribute('font-size', '24') - text.setAttribute('font-weight', 'bold') - text.textContent = '₿' - - svg.appendChild(text) - - // Add name and percentage below icon for targets and source_remaining - if (type === 'target' || type === 'source_remaining') { - // Add name text - if (targetName) { - const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - nameText.setAttribute('x', x) - nameText.setAttribute('y', y + 45) - nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : (isDarkTheme ? '#f3f4f6' : '#374151')) - nameText.setAttribute('font-family', 'Arial, sans-serif') - nameText.setAttribute('font-size', '14px') - nameText.setAttribute('font-weight', 'bold') - nameText.textContent = targetName - - svg.appendChild(nameText) - } - - // Add percentage text below name - const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') - percentText.setAttribute('x', x) - percentText.setAttribute('y', y + 65) - percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') - percentText.setAttribute('font-family', 'Arial, sans-serif') - percentText.setAttribute('font-size', '16px') - percentText.setAttribute('font-weight', 'bold') + percentText.setAttribute('class', 'text-h5') percentText.textContent = `${percentage}%` svg.appendChild(percentText) @@ -384,9 +320,6 @@ window.SplitPaymentsChart = Vue.defineComponent({ incomingText.setAttribute('y', y - 50) incomingText.setAttribute('text-anchor', 'middle') incomingText.setAttribute('fill', isDarkTheme ? '#f9ca24' : '#f7931a') - incomingText.setAttribute('font-family', 'Arial, sans-serif') - incomingText.setAttribute('font-size', '16px') - incomingText.setAttribute('font-weight', 'bold') incomingText.textContent = 'Incoming Payment' svg.appendChild(incomingText) From 0b1e6d5e7801c6d16335a3dbd0dd0b2254e1c14e Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:09:13 +0100 Subject: [PATCH 27/35] Renamed chart.js to split-payments-chart.js --- static/js/{chart.js => split-payments-chart.js} | 0 templates/splitpayments/index.html | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename static/js/{chart.js => split-payments-chart.js} (100%) diff --git a/static/js/chart.js b/static/js/split-payments-chart.js similarity index 100% rename from static/js/chart.js rename to static/js/split-payments-chart.js diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 65f4f84..90f1873 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -1128,6 +1128,6 @@
{% endblock %} {% block scripts %} {{ window_vars(user) }} - + {% endblock %} \ No newline at end of file From 0fdd739808b688245007ef220c1f6ad61a1d53cd Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:10:58 +0100 Subject: [PATCH 28/35] Moved styles into standalone file --- static/css/index.css | 573 ++++++++++++++++++++++++++++ templates/splitpayments/index.html | 576 +---------------------------- 2 files changed, 574 insertions(+), 575 deletions(-) create mode 100644 static/css/index.css diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..befe770 --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,573 @@ +.q-avatar { + width: 1.2em; + height: 1.2em; + } + .q-stepper__dot { + width: 42px; + height: 42px; + } + .q-stepper__content { + display: none; + } +.flow-chart-container { + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + background: var(--q-color-grey-3); + border-radius: 12px; + margin: 16px 0; + min-height: 400px; +} + +.body--dark .flow-chart-container { + background: var(--q-color-grey-9); +} + +.flow-chart { + width: 100%; + height: 100%; + max-width: 600px; + min-height: 400px; +} + +@media (max-width: 600px) { + .flow-chart-container { + display: none; + } +} + +/* Step 1 Mobile Improvements */ +.step-card { + margin-bottom: 16px; +} + +.step-section { + padding: 16px; +} + +.step-title { + font-size: 1.5rem; + line-height: 1.2; +} + +.step-description { + font-size: 1rem; + line-height: 1.4; +} + +/* Wallet selection improvements */ +.wallet-option { + cursor: pointer; + transition: all 0.2s ease; + min-height: 60px; +} + +.wallet-option:hover { + background-color: #f5f5f5; +} + +.wallet-option.selected { + border-color: #1976d2; + background-color: #e3f2fd; +} + +.wallet-option .q-radio { + min-height: 48px; +} + +/* Info banner improvements */ +.info-banner { + margin-top: 16px; +} + +/* Continue button improvements */ +.continue-button-container { + padding: 16px 0; +} + +.continue-button { + min-height: 48px; + padding: 0 24px; +} + +/* Mobile-specific styles */ +@media (max-width: 768px) { + .step-section { + padding: 12px; + } + + .step-title { + font-size: 1.25rem; + margin-bottom: 8px; + } + + .step-description { + font-size: 0.875rem; + margin-bottom: 16px; + } + + .wallet-option { + min-height: 64px; + } + + .wallet-option .q-card-section { + padding: 16px; + } + + .continue-button { + width: 100%; + min-height: 56px; + font-size: 1rem; + } + + .continue-button-container { + padding: 20px 0; + } + + .info-banner { + margin-top: 12px; + padding: 12px; + } + + /* Stepper improvements on mobile */ + .q-stepper--vertical .q-stepper__step { + padding: 8px 0; + } + + .q-stepper--vertical .q-stepper__dot { + width: 32px; + height: 32px; + } + + /* Reduce wizard header padding on mobile */ + .step-card .q-card-section:first-child { + padding-top: 8px; + } +} + +/* Step 2 Mobile Improvements */ +.split-recipient-container { + border-left: 3px solid #1976d2; + padding-left: 16px; + margin-bottom: 24px; + position: relative; +} + +.split-recipient-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.split-number-circle { + width: 28px; + height: 28px; + border-radius: 50%; + background-color: #1976d2; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 14px; + flex-shrink: 0; +} + +.split-header-text { + font-size: 1rem; + font-weight: 500; + color: #374151; + flex: 1; +} + +.remove-btn { + opacity: 0.7; + transition: opacity 0.2s ease; +} + +.remove-btn:hover { + opacity: 1; +} + +.split-fields { + display: flex; + flex-direction: column; + gap: 12px; +} + +.split-field { + width: 100%; +} + +.split-field .q-field__control { + min-height: 48px; +} + +.add-recipient-container { + display: flex; + justify-content: center; + padding: 16px 0; +} + +.add-recipient-btn { + min-height: 48px; + padding: 0 24px; +} + +.step-navigation { + display: flex; + justify-content: space-between; + align-items: center; + gap: 16px; + padding: 16px 0; +} + +.back-button { + min-height: 48px; + padding: 0 20px; +} + +/* Mobile-specific styles for Step 2 */ +@media (max-width: 768px) { + .split-recipient-container { + border-left: 2px solid #1976d2; + padding-left: 12px; + margin-bottom: 20px; + } + + .split-recipient-header { + gap: 8px; + margin-bottom: 12px; + } + + .split-number-circle { + width: 24px; + height: 24px; + font-size: 12px; + } + + .split-header-text { + font-size: 0.875rem; + } + + .split-fields { + gap: 8px; + } + + .split-field .q-field__control { + min-height: 52px; + } + + .add-recipient-btn { + width: 100%; + min-height: 56px; + font-size: 1rem; + } + + .step-navigation { + flex-direction: column; + gap: 12px; + padding: 20px 0; + } + + .back-button { + width: 100%; + min-height: 52px; + order: 2; + } + + .continue-button { + width: 100%; + min-height: 56px; + order: 1; + } + + /* Improve percentage badge layout on mobile */ + .items-center.q-gutter-md { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + .items-center.q-gutter-md .q-badge { + align-self: flex-start; + } +} + +/* Step 3 Mobile Improvements */ +.summary-cards-container { + display: flex; + flex-direction: column; + gap: 16px; +} + +/* Ensure consistent font family for all Step 3 elements */ +.summary-card-title, +.summary-card-main-text, +.summary-card-sub-text, +.target-name, +.target-wallet { + font-family: Roboto, "-apple-system", "Helvetica Neue", Helvetica, Arial, sans-serif; +} + +.summary-card { + border-radius: 8px; + overflow: hidden; +} + +.summary-card-source { + background: var(--q-color-blue-1); +} + +.summary-card-targets { + background: var(--q-color-green-1); +} + +.body--dark .summary-card-source { + background: var(--q-color-blue-10); +} + +.body--dark .summary-card-targets { + background: var(--q-color-green-10); +} + +.summary-card-section { + padding: 16px; +} + +.summary-card-header { + display: flex; + align-items: center; + margin-bottom: 12px; + color: var(--q-color-primary); +} + +.summary-card-source .summary-card-header { + color: var(--q-color-blue-8); +} + +.summary-card-targets .summary-card-header { + color: var(--q-color-green-8); +} + +.body--dark .summary-card-source .summary-card-header { + color: var(--q-color-blue-4); +} + +.body--dark .summary-card-targets .summary-card-header { + color: var(--q-color-green-4); +} + +.summary-card-title { + margin-left: 10px; + margin-top: -5px; + font-weight: 600; + line-height: 1.2; +} + +.summary-card-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.summary-card-main-text { + font-size: 1.125rem; + font-weight: 700; + color: var(--q-color-primary); +} + +.summary-card-source .summary-card-main-text { + color: var(--q-color-blue-8); +} + +.summary-card-targets .summary-card-main-text { + color: var(--q-color-green-8); +} + +.body--dark .summary-card-source .summary-card-main-text { + color: var(--q-color-blue-4); +} + +.body--dark .summary-card-targets .summary-card-main-text { + color: var(--q-color-green-4); +} + +.summary-card-sub-text { + font-size: 0.875rem; + color: var(--q-color-grey-7); +} + +.body--dark .summary-card-sub-text { + color: var(--q-color-grey-5); +} + + +.detailed-targets-container { + margin-bottom: 24px; +} + +.targets-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.target-summary-item { + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + background: var(--q-color-grey-1); + border-radius: 8px; + border: 1px solid var(--q-color-grey-4); +} + +.body--dark .target-summary-item { + background: var(--q-color-grey-9); + border-color: var(--q-color-grey-7); +} + +.target-percent-circle { + width: 60px; + height: 60px; + border-radius: 50%; + background-color: #059669; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1rem; + flex-shrink: 0; +} + +.target-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.target-name { + font-size: 1rem; + font-weight: 600; + color: var(--q-color-grey-9); +} + +.body--dark .target-name { + color: var(--q-color-grey-3); +} + +.target-wallet { + font-size: 0.875rem; + color: var(--q-color-grey-7); + word-break: break-all; +} + +.body--dark .target-wallet { + color: var(--q-color-grey-5); +} + +.step-3-navigation { + justify-content: center; + gap: 16px; +} + +.confirm-button { + background: #059669 !important; + color: white !important; +} + +.confirm-button:disabled { + background: #9ca3af !important; +} + +/* Mobile-specific styles for Step 3 */ +@media (max-width: 768px) { + .summary-cards-container { + gap: 12px; + } + + .summary-card-section { + padding: 12px; + } + + .summary-card-header { + margin-bottom: 8px; + } + + .summary-card-title { + font-size: 0.875rem; + line-height: 1.2; + } + + .summary-card-main-text { + font-size: 1rem; + } + + .summary-card-sub-text { + font-size: 0.75rem; + } + + .targets-list { + gap: 8px; + } + + .target-summary-item { + padding: 12px; + gap: 12px; + } + + .target-percent-circle { + width: 48px; + height: 48px; + font-size: 0.875rem; + } + + .target-name { + font-size: 0.875rem; + } + + .target-wallet { + font-size: 0.75rem; + } + + .step-3-navigation { + flex-direction: column; + gap: 12px; + } + + .back-button { + width: 100%; + min-height: 52px; + order: 2; + } + + .confirm-button { + width: 100%; + min-height: 56px; + order: 1; + font-size: 1rem; + } + + /* Flow chart container mobile optimization */ + .flow-chart-container { + margin: 12px 0; + padding: 12px; + min-height: 300px; + } + + /* Status banners mobile optimization */ + .q-banner { + padding: 12px; + margin-bottom: 12px; + } + + .q-banner .q-banner__content { + font-size: 0.875rem; + line-height: 1.4; + } +} \ No newline at end of file diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 90f1873..816f928 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -550,581 +550,7 @@
{% endblock %} {% block styles %} - + {% endblock %} {% block scripts %} {{ window_vars(user) }} From ce3bcafdf537d6bcccf21742c611dc3850bdf089 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:27:43 +0100 Subject: [PATCH 29/35] Added split payments summary to each wallet listed in step 1 --- static/js/index.js | 45 +++++++++++++++++++++++++----- templates/splitpayments/index.html | 19 +++++++++---- 2 files changed, 52 insertions(+), 12 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 9b0bccb..7c02d44 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -33,7 +33,8 @@ window.app = Vue.createApp({ // Existing data selectedWallet: null, currentHash: '', // a string that must match if the edit data is unchanged - targets: [] + targets: [], + walletSplits: {} // Store split data for each wallet } }, computed: { @@ -133,6 +134,28 @@ window.app = Vue.createApp({ }, isDirty() { return hashTargets(this.targets) !== this.currentHash + }, + + // Get split summaries for all wallets + walletSplitSummaries() { + const summaries = {} + + for (const walletId in this.walletSplits) { + const splits = this.walletSplits[walletId] + if (splits && splits.length > 0) { + const totalPercent = splits.reduce((sum, split) => sum + (split.percent || 0), 0) + const remainingPercent = Math.max(0, 100 - totalPercent) + + summaries[walletId] = { + totalPercent, + remainingPercent, + splitCount: splits.length, + splits: splits.slice(0, 3) // Show first 3 splits + } + } + } + + return summaries } }, methods: { @@ -328,6 +351,8 @@ window.app = Vue.createApp({ }, async checkExistingConfigurations() { + let firstWalletWithSplits = null + // Check each wallet for existing split payment configurations for (const wallet of this.g.user.wallets) { try { @@ -337,10 +362,13 @@ window.app = Vue.createApp({ wallet.adminkey ) if (response.data && response.data.length > 0) { - // Found existing configuration, select this wallet - this.selectedWallet = wallet - this.getTargets() - return + // Store split data for this wallet + this.walletSplits[wallet.id] = response.data + + // Remember the first wallet with splits + if (!firstWalletWithSplits) { + firstWalletWithSplits = wallet + } } } catch (err) { // Wallet has no configuration, continue checking others @@ -348,8 +376,11 @@ window.app = Vue.createApp({ } } - // No existing configurations found, select first wallet - if (this.g.user.wallets.length > 0) { + // Select first wallet with splits, or first wallet if none have splits + if (firstWalletWithSplits) { + this.selectedWallet = firstWalletWithSplits + this.getTargets() + } else if (this.g.user.wallets.length > 0) { this.selectedWallet = this.g.user.wallets[0] } } diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 816f928..39bc951 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -59,12 +59,21 @@ :val="wallet" color="primary" size="md" - class="q-mr-md" /> -
-
{% raw %}{{ wallet.name }}{% endraw %}
-
{% raw %}{{ wallet.id }}{% endraw %}
-
+
+
+
{% raw %}{{ wallet.name }}{% endraw %}
+
{% raw %}{{ wallet.id }}{% endraw %}
+
+
+
+ + {% raw %}{{ walletSplitSummaries[wallet.id].splitCount }}{% endraw %} split{% raw %}{{ walletSplitSummaries[wallet.id].splitCount !== 1 ? 's' : '' }}{% endraw %} + ({% raw %}{{ walletSplitSummaries[wallet.id].totalPercent }}%{% endraw %}) + +
+
+
From 4a1197180cf9d61bd92450f92d4773a73ed6b332 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:36:53 +0100 Subject: [PATCH 30/35] Updated notices on step 3 and add confirmation message after saving --- static/js/index.js | 10 ++++++- templates/splitpayments/index.html | 47 +++++++++++++++++++----------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 7c02d44..4439f27 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -34,7 +34,9 @@ window.app = Vue.createApp({ selectedWallet: null, currentHash: '', // a string that must match if the edit data is unchanged targets: [], - walletSplits: {} // Store split data for each wallet + walletSplits: {}, // Store split data for each wallet + showSavedConfirmation: false, // Show confirmation after saving + lastSavedTargetCount: 0 // Track number of targets that were saved } }, computed: { @@ -163,6 +165,7 @@ window.app = Vue.createApp({ nextStep() { if (this.currentStep < this.maxSteps) { if (this.currentStep === 1 && this.canProceedFromStep1) { + this.showSavedConfirmation = false // Hide confirmation when proceeding this.currentStep++ this.scrollToTop() } else if (this.currentStep === 2 && this.canProceedFromStep2) { @@ -313,6 +316,11 @@ window.app = Vue.createApp({ }) // Update hash to reflect saved state this.currentHash = hashTargets(this.targets) + // Update wallet splits data + this.walletSplits[this.selectedWallet.id] = [...this.targets] + // Show confirmation banner on step 1 + this.showSavedConfirmation = true + this.lastSavedTargetCount = this.targets.length // Reset to step 1 after successful save this.currentStep = 1 this.scrollToTop() diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 39bc951..78b37fc 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -31,6 +31,34 @@
Select Source Wallet

Choose the wallet from which payments will be split.

+ + + + +
+ Split payments successfully activated! +
+
+ {% raw %}{{ lastSavedTargetCount }}{% endraw %} split target{% raw %}{{ lastSavedTargetCount !== 1 ? 's' : '' }}{% endraw %} configured for + {% raw %}{{ selectedWallet ? selectedWallet.name : 'the selected wallet' }}{% endraw %}. +
+ +
@@ -415,29 +443,14 @@ - - - -
Configuration ready!
-
- Your split payment configuration is valid and ready to activate. -
-
- - +
Important:
-

• Splits totaling 100% may fail due to Lightning routing fees. Please keep this in mind when setting up splits for a wallet where small payments are common. +

• Splits totaling close to 100% may fail due to Lightning routing fees. Please keep this in mind when setting up splits for a wallet where small payments are common.
• Each payment to this wallet will be automatically split when it arrives

From fcc83e470b2820f6dc7378c20be3ac90055a3a6b Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:40:18 +0100 Subject: [PATCH 31/35] Chart shows a single payment if no splits are set up --- static/js/index.js | 21 +++++++++++---------- static/js/split-payments-chart.js | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index 4439f27..a1885e9 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -110,16 +110,6 @@ window.app = Vue.createApp({ splitDiagramData() { const data = [] - // Add source wallet (remaining percentage) - if (this.remainingPercent > 0) { - data.push({ - name: this.selectedWallet ? this.selectedWallet.name : 'Source', - percent: this.remainingPercent, - type: 'source', - color: '#1976d2' - }) - } - // Add target wallets this.targets.forEach(target => { if (target.percent > 0 && target.alias) { @@ -132,6 +122,17 @@ window.app = Vue.createApp({ } }) + // Add source wallet (remaining percentage or 100% if no targets) + const remainingPercent = this.targets.length > 0 ? this.remainingPercent : 100 + if (remainingPercent > 0) { + data.push({ + name: this.selectedWallet ? this.selectedWallet.name : 'Source', + percent: remainingPercent, + type: 'source', + color: '#1976d2' + }) + } + return data.sort((a, b) => b.percent - a.percent) }, isDirty() { diff --git a/static/js/split-payments-chart.js b/static/js/split-payments-chart.js index b142f30..dbfed9a 100644 --- a/static/js/split-payments-chart.js +++ b/static/js/split-payments-chart.js @@ -54,7 +54,7 @@ window.SplitPaymentsChart = Vue.defineComponent({ const targets = this.splitDiagramData.filter(item => item.type === 'target') const sourceRemaining = this.splitDiagramData.filter(item => item.type === 'source') - if (targets.length === 0) { + if (targets.length === 0 && sourceRemaining.length === 0) { container.appendChild(svg) return } From c45ff4986bd89b0b4dd33fb7ba8e88a924331032 Mon Sep 17 00:00:00 2001 From: blackcoffeexbt <87530449+blackcoffeexbt@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:50:31 +0100 Subject: [PATCH 32/35] Update warning text --- templates/splitpayments/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 78b37fc..8650fbe 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -169,7 +169,7 @@
High percentage warning
- Splits totaling close to 100% may fail for some recipients due to Lightning routing fees. Consider reducing the total to 95% or less. + Splits totaling close to 100% may fail for some recipients due to Lightning routing fees. Consider reducing the total to 95% or less if you will be sending small payments.
From 1dee137bc53eca0c64b96c41e2ef09835e45ea7f Mon Sep 17 00:00:00 2001 From: Tiago Vasconcelos Date: Tue, 5 May 2026 23:08:19 +0100 Subject: [PATCH 33/35] rebase and lint --- static/css/index.css | 26 +-- static/js/index.js | 142 +++++++++----- static/js/split-payments-chart.js | 271 +++++++++++++++++++-------- templates/splitpayments/index.html | 291 +++++++++++++++++++---------- 4 files changed, 481 insertions(+), 249 deletions(-) diff --git a/static/css/index.css b/static/css/index.css index befe770..16c2610 100644 --- a/static/css/index.css +++ b/static/css/index.css @@ -1,14 +1,14 @@ .q-avatar { - width: 1.2em; - height: 1.2em; - } - .q-stepper__dot { - width: 42px; - height: 42px; - } - .q-stepper__content { - display: none; - } + width: 1.2em; + height: 1.2em; +} +.q-stepper__dot { + width: 42px; + height: 42px; +} +.q-stepper__content { + display: none; +} .flow-chart-container { display: flex; justify-content: center; @@ -309,7 +309,8 @@ .summary-card-sub-text, .target-name, .target-wallet { - font-family: Roboto, "-apple-system", "Helvetica Neue", Helvetica, Arial, sans-serif; + font-family: Roboto, '-apple-system', 'Helvetica Neue', Helvetica, Arial, + sans-serif; } .summary-card { @@ -404,7 +405,6 @@ color: var(--q-color-grey-5); } - .detailed-targets-container { margin-bottom: 24px; } @@ -570,4 +570,4 @@ font-size: 0.875rem; line-height: 1.4; } -} \ No newline at end of file +} diff --git a/static/js/index.js b/static/js/index.js index a1885e9..4a01bbb 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -22,14 +22,14 @@ window.app = Vue.createApp({ watch: { selectedWallet() { this.getTargets() - }, + } }, data() { return { // Wizard state currentStep: 1, maxSteps: 3, - + // Existing data selectedWallet: null, currentHash: '', // a string that must match if the edit data is unchanged @@ -45,40 +45,64 @@ window.app = Vue.createApp({ return this.selectedWallet !== null }, canProceedFromStep2() { - return this.targets.length > 0 && this.totalPercent <= 100 && this.allTargetsValid + return ( + this.targets.length > 0 && + this.totalPercent <= 100 && + this.allTargetsValid + ) }, totalPercent() { - return this.targets.reduce((sum, target) => sum + (target.percent || 0), 0) + return this.targets.reduce( + (sum, target) => sum + (target.percent || 0), + 0 + ) }, remainingPercent() { return Math.max(0, 100 - this.totalPercent) }, allTargetsValid() { - return this.targets.every(target => - target.wallet && target.wallet.trim() !== '' && - target.percent > 0 && target.percent <= 100 && - target.alias && target.alias.trim() !== '' && target.alias.trim().length <= 50 - ) && !this.hasDuplicateRecipients && !this.hasDuplicateNames + return ( + this.targets.every( + target => + target.wallet && + target.wallet.trim() !== '' && + target.percent > 0 && + target.percent <= 100 && + target.alias && + target.alias.trim() !== '' && + target.alias.trim().length <= 50 + ) && + !this.hasDuplicateRecipients && + !this.hasDuplicateNames + ) }, hasValidationErrors() { - return this.targets.some(target => - !target.wallet || target.wallet.trim() === '' || - !target.alias || target.alias.trim() === '' || - target.percent <= 0 || target.percent > 100 - ) || this.hasDuplicateRecipients || this.hasDuplicateNames + return ( + this.targets.some( + target => + !target.wallet || + target.wallet.trim() === '' || + !target.alias || + target.alias.trim() === '' || + target.percent <= 0 || + target.percent > 100 + ) || + this.hasDuplicateRecipients || + this.hasDuplicateNames + ) }, hasDuplicateRecipients() { const walletAddresses = this.targets .filter(target => target.wallet && target.wallet.trim() !== '') .map(target => target.wallet.trim().toLowerCase()) - + return walletAddresses.length !== new Set(walletAddresses).size }, hasDuplicateNames() { const splitNames = this.targets .filter(target => target.alias && target.alias.trim() !== '') .map(target => target.alias.trim().toLowerCase()) - + return splitNames.length !== new Set(splitNames).size }, validationSummary() { @@ -90,12 +114,20 @@ window.app = Vue.createApp({ errors.push(`Total percentage (${this.totalPercent}%) exceeds 100%`) } if (this.hasDuplicateRecipients) { - errors.push('Duplicate recipient addresses found - each recipient must be unique') + errors.push( + 'Duplicate recipient addresses found - each recipient must be unique' + ) } if (this.hasDuplicateNames) { - errors.push('Duplicate split names found - each split name must be unique') + errors.push( + 'Duplicate split names found - each split name must be unique' + ) } - if (this.hasValidationErrors && !this.hasDuplicateRecipients && !this.hasDuplicateNames) { + if ( + this.hasValidationErrors && + !this.hasDuplicateRecipients && + !this.hasDuplicateNames + ) { errors.push('Some fields have validation errors') } return errors @@ -109,7 +141,7 @@ window.app = Vue.createApp({ // Split diagram data splitDiagramData() { const data = [] - + // Add target wallets this.targets.forEach(target => { if (target.percent > 0 && target.alias) { @@ -121,9 +153,10 @@ window.app = Vue.createApp({ }) } }) - + // Add source wallet (remaining percentage or 100% if no targets) - const remainingPercent = this.targets.length > 0 ? this.remainingPercent : 100 + const remainingPercent = + this.targets.length > 0 ? this.remainingPercent : 100 if (remainingPercent > 0) { data.push({ name: this.selectedWallet ? this.selectedWallet.name : 'Source', @@ -132,23 +165,26 @@ window.app = Vue.createApp({ color: '#1976d2' }) } - + return data.sort((a, b) => b.percent - a.percent) }, isDirty() { return hashTargets(this.targets) !== this.currentHash }, - + // Get split summaries for all wallets walletSplitSummaries() { const summaries = {} - + for (const walletId in this.walletSplits) { const splits = this.walletSplits[walletId] if (splits && splits.length > 0) { - const totalPercent = splits.reduce((sum, split) => sum + (split.percent || 0), 0) + const totalPercent = splits.reduce( + (sum, split) => sum + (split.percent || 0), + 0 + ) const remainingPercent = Math.max(0, 100 - totalPercent) - + summaries[walletId] = { totalPercent, remainingPercent, @@ -157,7 +193,7 @@ window.app = Vue.createApp({ } } } - + return summaries } }, @@ -187,47 +223,50 @@ window.app = Vue.createApp({ this.scrollToTop() } }, - + // Scroll to top of wizard scrollToTop() { this.$nextTick(() => { // Find the wizard container (the stepper card) - const wizardElement = document.querySelector('.q-stepper') || document.querySelector('.q-card') + const wizardElement = + document.querySelector('.q-stepper') || + document.querySelector('.q-card') if (wizardElement) { - wizardElement.scrollIntoView({ - behavior: 'smooth', + wizardElement.scrollIntoView({ + behavior: 'smooth', block: 'start', - inline: 'nearest' + inline: 'nearest' }) } else { // Fallback to window scroll - window.scrollTo({ - top: 0, - behavior: 'smooth' + window.scrollTo({ + top: 0, + behavior: 'smooth' }) } }) }, - + // Validation helper methods isDuplicateRecipient(index) { const currentWallet = this.targets[index]?.wallet?.trim().toLowerCase() if (!currentWallet) return false - - return this.targets.some((target, i) => - i !== index && target.wallet?.trim().toLowerCase() === currentWallet + + return this.targets.some( + (target, i) => + i !== index && target.wallet?.trim().toLowerCase() === currentWallet ) }, isDuplicateName(index) { const currentName = this.targets[index]?.alias?.trim().toLowerCase() if (!currentName) return false - - return this.targets.some((target, i) => - i !== index && target.alias?.trim().toLowerCase() === currentName + + return this.targets.some( + (target, i) => + i !== index && target.alias?.trim().toLowerCase() === currentName ) }, - - + // Target management methods clearTarget(index) { if (this.targets.length == 1) { @@ -329,7 +368,8 @@ window.app = Vue.createApp({ .catch(err => { LNbits.utils.notifyApiError(err) Quasar.Notify.create({ - message: 'Failed to save split payment configuration. Please try again.', + message: + 'Failed to save split payment configuration. Please try again.', timeout: 5000, color: 'negative', icon: 'error' @@ -358,10 +398,10 @@ window.app = Vue.createApp({ }) }) }, - + async checkExistingConfigurations() { let firstWalletWithSplits = null - + // Check each wallet for existing split payment configurations for (const wallet of this.g.user.wallets) { try { @@ -373,7 +413,7 @@ window.app = Vue.createApp({ if (response.data && response.data.length > 0) { // Store split data for this wallet this.walletSplits[wallet.id] = response.data - + // Remember the first wallet with splits if (!firstWalletWithSplits) { firstWalletWithSplits = wallet @@ -384,7 +424,7 @@ window.app = Vue.createApp({ continue } } - + // Select first wallet with splits, or first wallet if none have splits if (firstWalletWithSplits) { this.selectedWallet = firstWalletWithSplits @@ -396,5 +436,5 @@ window.app = Vue.createApp({ }, mounted() { this.checkExistingConfigurations() - }, + } }) diff --git a/static/js/split-payments-chart.js b/static/js/split-payments-chart.js index dbfed9a..cdb8404 100644 --- a/static/js/split-payments-chart.js +++ b/static/js/split-payments-chart.js @@ -36,34 +36,41 @@ window.SplitPaymentsChart = Vue.defineComponent({ console.warn('Chart container not found') return } - + // Clear previous content container.innerHTML = '' - + // Detect dark theme const isDarkTheme = document.body.classList.contains('body--dark') - + // Create SVG element - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg') + const svg = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'svg' + ) svg.setAttribute('width', '100%') svg.setAttribute('height', '500') svg.setAttribute('viewBox', '0 0 400 450') svg.style.background = 'transparent' - + // Get targets data and source data - const targets = this.splitDiagramData.filter(item => item.type === 'target') - const sourceRemaining = this.splitDiagramData.filter(item => item.type === 'source') - + const targets = this.splitDiagramData.filter( + item => item.type === 'target' + ) + const sourceRemaining = this.splitDiagramData.filter( + item => item.type === 'source' + ) + if (targets.length === 0 && sourceRemaining.length === 0) { container.appendChild(svg) return } - + // Define positions const sourceX = 200 const sourceY = 80 const targetY = 320 - + // Calculate bottom row items (targets + source if remaining > 0) const bottomRowItems = [...targets] if (sourceRemaining.length > 0 && this.remainingPercent > 0) { @@ -74,121 +81,172 @@ window.SplitPaymentsChart = Vue.defineComponent({ color: '#96A6FF' }) } - + // Calculate positions for bottom row items const bottomRowPositions = [] if (bottomRowItems.length === 1) { - bottomRowPositions.push({ x: sourceX, y: targetY }) + bottomRowPositions.push({x: sourceX, y: targetY}) } else { // Use the full width of the SVG viewBox (400px) with padding const padding = 10 // Padding from edges - const totalWidth = 400 - (padding * 2) // Available width + const totalWidth = 400 - padding * 2 // Available width const spacing = totalWidth / (bottomRowItems.length - 1) const startX = padding - + bottomRowItems.forEach((item, index) => { - bottomRowPositions.push({ x: startX + (index * spacing), y: targetY }) + bottomRowPositions.push({x: startX + index * spacing, y: targetY}) }) } - + // Calculate proportional line thickness const maxPercent = Math.max(...bottomRowItems.map(t => t.percent)) const maxThickness = 30 // Maximum line thickness in pixels - + // Draw flowing lines - source_remaining lines first (behind other lines) // First pass: draw source_remaining lines bottomRowItems.forEach((item, index) => { if (item.type === 'source_remaining') { const itemPos = bottomRowPositions[index] // Calculate thickness proportional to the highest percentage - const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - + const lineThickness = Math.max( + 3, + (item.percent / maxPercent) * maxThickness + ) + // End the line before the wallet icon (30px is wallet icon radius) const lineEndY = targetY - 45 - this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') + this.drawFlowingLine( + svg, + sourceX, + sourceY + 35, + itemPos.x, + lineEndY, + lineThickness, + item.color || '#4ade80' + ) } }) - + // Second pass: draw target lines (on top of source_remaining lines) bottomRowItems.forEach((item, index) => { if (item.type === 'target') { const itemPos = bottomRowPositions[index] // Calculate thickness proportional to the highest percentage - const lineThickness = Math.max(3, (item.percent / maxPercent) * maxThickness) - + const lineThickness = Math.max( + 3, + (item.percent / maxPercent) * maxThickness + ) + // End the line before the wallet icon (30px is wallet icon radius) const lineEndY = targetY - 45 - this.drawFlowingLine(svg, sourceX, sourceY + 35, itemPos.x, lineEndY, lineThickness, item.color || '#4ade80') + this.drawFlowingLine( + svg, + sourceX, + sourceY + 35, + itemPos.x, + lineEndY, + lineThickness, + item.color || '#4ade80' + ) } }) - + // Draw source Bitcoin logo this.drawBitcoinLogo(svg, sourceX, sourceY, isDarkTheme) - + // Draw bottom row wallet icons bottomRowItems.forEach((item, index) => { const itemPos = bottomRowPositions[index] if (item.type === 'source_remaining') { - this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'source_remaining', item.percent, item.name, isDarkTheme) + this.drawWalletIcon( + svg, + itemPos.x, + itemPos.y, + 'source_remaining', + item.percent, + item.name, + isDarkTheme + ) } else { - this.drawWalletIcon(svg, itemPos.x, itemPos.y, 'target', item.percent, item.name, isDarkTheme) + this.drawWalletIcon( + svg, + itemPos.x, + itemPos.y, + 'target', + item.percent, + item.name, + isDarkTheme + ) } }) - + container.appendChild(svg) console.log('Flow chart created successfully') } catch (error) { console.error('Error creating flow chart:', error) } }, - + drawFlowingLine(svg, x1, y1, x2, y2, finalThickness, color) { // Create a tapered line that starts at 10px and increases to finalThickness const startThickness = 10 const segments = 20 // Number of segments for smooth taper const midY = y1 + (y2 - y1) * 0.6 - + // Generate points along the quadratic Bezier curve const points = [] for (let i = 0; i <= segments; i++) { const t = i / segments let x, y - + if (t <= 0.5) { // First quadratic curve: (x1, y1) to ((x1+x2)/2, midY) const localT = t * 2 const p0 = {x: x1, y: y1} const p1 = {x: x1, y: midY} const p2 = {x: (x1 + x2) / 2, y: midY} - - x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x - y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y + + x = + (1 - localT) * (1 - localT) * p0.x + + 2 * (1 - localT) * localT * p1.x + + localT * localT * p2.x + y = + (1 - localT) * (1 - localT) * p0.y + + 2 * (1 - localT) * localT * p1.y + + localT * localT * p2.y } else { // Second quadratic curve: ((x1+x2)/2, midY) to (x2, y2) const localT = (t - 0.5) * 2 const p0 = {x: (x1 + x2) / 2, y: midY} const p1 = {x: x2, y: midY} const p2 = {x: x2, y: y2} - - x = (1 - localT) * (1 - localT) * p0.x + 2 * (1 - localT) * localT * p1.x + localT * localT * p2.x - y = (1 - localT) * (1 - localT) * p0.y + 2 * (1 - localT) * localT * p1.y + localT * localT * p2.y + + x = + (1 - localT) * (1 - localT) * p0.x + + 2 * (1 - localT) * localT * p1.x + + localT * localT * p2.x + y = + (1 - localT) * (1 - localT) * p0.y + + 2 * (1 - localT) * localT * p1.y + + localT * localT * p2.y } - + // Calculate thickness at this point const thickness = startThickness + (finalThickness - startThickness) * t points.push({x, y, thickness}) } - + // Create polygon points for the tapered line const leftPoints = [] const rightPoints = [] - + for (let i = 0; i < points.length; i++) { const point = points[i] const halfThickness = point.thickness / 2 - + // Calculate direction vector - let dx = 0, dy = 1 + let dx = 0, + dy = 1 if (i < points.length - 1) { dx = points[i + 1].x - point.x dy = points[i + 1].y - point.y @@ -196,18 +254,18 @@ window.SplitPaymentsChart = Vue.defineComponent({ dx = point.x - points[i - 1].x dy = point.y - points[i - 1].y } - + // Normalize direction vector const length = Math.sqrt(dx * dx + dy * dy) if (length > 0) { dx /= length dy /= length } - + // Calculate perpendicular offset (rotate 90 degrees) const perpX = -dy const perpY = dx - + // Add points to left and right sides leftPoints.push({ x: point.x + perpX * halfThickness, @@ -218,117 +276,164 @@ window.SplitPaymentsChart = Vue.defineComponent({ y: point.y - perpY * halfThickness }) } - + // Create arrow tip pointing down const lastPoint = points[points.length - 1] const arrowHeight = 15 // Fixed arrow height so all arrows terminate at same Y position - + // Get the last left and right points to connect seamlessly const lastLeftPoint = leftPoints[leftPoints.length - 1] const lastRightPoint = rightPoints[0] // rightPoints is reversed, so first element is the last point - + // Arrow tip points - connect directly to the line ends const arrowTip = {x: lastPoint.x, y: lastPoint.y + arrowHeight} - + // Create combined polygon including line body and arrow const allPoints = [ ...leftPoints.slice(0, -1), // All left points except the last one lastLeftPoint, // Last left point arrowTip, // Arrow tip - lastRightPoint, // Last right point + lastRightPoint, // Last right point ...rightPoints.slice(1) // All right points except the first one ] - - const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon') - const pointsString = allPoints.map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ') - + + const polygon = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'polygon' + ) + const pointsString = allPoints + .map(p => `${p.x.toFixed(2)},${p.y.toFixed(2)}`) + .join(' ') + polygon.setAttribute('points', pointsString) polygon.setAttribute('fill', color) polygon.setAttribute('opacity', '1') - + svg.appendChild(polygon) }, - - drawWalletIcon(svg, x, y, type, percentage, targetName = null, isDarkTheme = false) { + + drawWalletIcon( + svg, + x, + y, + type, + percentage, + targetName = null, + isDarkTheme = false + ) { // Create wallet icon using the PNG image - const image = document.createElementNS('http://www.w3.org/2000/svg', 'image') + const image = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'image' + ) image.setAttribute('x', x - 30) image.setAttribute('y', y - 30) image.setAttribute('width', 60) image.setAttribute('height', 60) - + image.setAttribute('href', '/splitpayments/static/image/icon-wallet.png') - + // Add color filter for source vs target distinction if (type === 'source' || type === 'source_remaining') { // Add blue tint for source wallet image.setAttribute('style', 'filter: hue-rotate(200deg) saturate(1.2)') } - + // Add error handling - if image fails to load, show a fallback image.addEventListener('error', () => { console.warn('Failed to load wallet icon, using fallback') // Remove the broken image and replace with a styled rectangle svg.removeChild(image) }) - + svg.appendChild(image) - + // Add name and percentage below icon for targets and source_remaining if (type === 'target' || type === 'source_remaining') { // Add name text if (targetName) { - const nameText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + const nameText = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'text' + ) nameText.setAttribute('x', x) nameText.setAttribute('y', y + 55) nameText.setAttribute('text-anchor', 'middle') - nameText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : (isDarkTheme ? '#f3f4f6' : '#374151')) + nameText.setAttribute( + 'fill', + type === 'source_remaining' + ? '#96A6FF' + : isDarkTheme + ? '#f3f4f6' + : '#374151' + ) nameText.setAttribute('class', 'text-body2') nameText.textContent = targetName - + svg.appendChild(nameText) } - + // Add percentage text below name - const percentText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + const percentText = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'text' + ) percentText.setAttribute('x', x) percentText.setAttribute('y', y + 85) percentText.setAttribute('text-anchor', 'middle') - percentText.setAttribute('fill', type === 'source_remaining' ? '#96A6FF' : '#f59e0b') + percentText.setAttribute( + 'fill', + type === 'source_remaining' ? '#96A6FF' : '#f59e0b' + ) percentText.setAttribute('class', 'text-h5') percentText.textContent = `${percentage}%` - + svg.appendChild(percentText) } }, - + drawBitcoinLogo(svg, x, y, isDarkTheme = false) { // Create Bitcoin logo using SVG - const logoGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g') - - const bitcoinPath = document.createElementNS('http://www.w3.org/2000/svg', 'path') - bitcoinPath.setAttribute('d', 'M39.0674606,19.3675957 L40.5054606,13.5995957 L36.9944606,12.7245957 L35.5944606,18.3405957 C34.6714606,18.1105957 33.7234606,17.8935957 32.7814606,17.6785957 L34.1914606,12.0255957 L30.6824606,11.1505957 L29.2434606,16.9165957 C28.4794606,16.7425957 27.7294606,16.5705957 27.0014606,16.3895957 L27.0054606,16.3715957 L22.1634606,15.1625957 L21.2294606,18.9125957 C21.2294606,18.9125957 23.8344606,19.5095957 23.7794606,19.5465957 C25.2014606,19.9015957 25.4584606,20.8425957 25.4154606,21.5885957 L23.7774606,28.1595957 L23.7714606,28.1845957 L21.4754606,37.3895957 C21.3014606,37.8215957 20.8604606,38.4695957 19.8664606,38.2235957 C19.9014606,38.2745957 17.3144606,37.5865957 17.3144606,37.5865957 L15.5714606,41.6055957 L20.1404606,42.7445957 C20.9904606,42.9575957 21.8234606,43.1805957 22.6434606,43.3905957 L21.1904606,49.2245957 L24.6974606,50.0995957 L26.1364606,44.3275957 C27.0944606,44.5875957 28.0244606,44.8275957 28.9344606,45.0535957 L27.5004606,50.7985957 L31.0114606,51.6735957 L32.4644606,45.8505957 C38.4514606,46.9835957 42.9534606,46.5265957 44.8484606,41.1115957 C46.3754606,36.7515957 44.7724606,34.2365957 41.6224606,32.5965957 C43.9164606,32.0675957 45.6444606,30.5585957 46.1054606,27.4415957 C46.7424606,23.1835957 43.5004606,20.8945957 39.0674606,19.3675957 Z M38.0834606,38.6905957 C36.9984606,43.0505957 29.6574606,40.6935957 27.2774606,40.1025957 L29.2054606,32.3735957 C31.5854606,32.9675957 39.2174606,34.1435957 38.0834606,38.6905957 Z M39.1694606,27.3785957 C38.1794606,31.3445957 32.0694606,29.3295957 30.0874606,28.8355957 L31.8354606,21.8255957 C33.8174606,22.3195957 40.2004606,23.2415957 39.1694606,27.3785957 Z') + const logoGroup = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'g' + ) + + const bitcoinPath = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'path' + ) + bitcoinPath.setAttribute( + 'd', + 'M39.0674606,19.3675957 L40.5054606,13.5995957 L36.9944606,12.7245957 L35.5944606,18.3405957 C34.6714606,18.1105957 33.7234606,17.8935957 32.7814606,17.6785957 L34.1914606,12.0255957 L30.6824606,11.1505957 L29.2434606,16.9165957 C28.4794606,16.7425957 27.7294606,16.5705957 27.0014606,16.3895957 L27.0054606,16.3715957 L22.1634606,15.1625957 L21.2294606,18.9125957 C21.2294606,18.9125957 23.8344606,19.5095957 23.7794606,19.5465957 C25.2014606,19.9015957 25.4584606,20.8425957 25.4154606,21.5885957 L23.7774606,28.1595957 L23.7714606,28.1845957 L21.4754606,37.3895957 C21.3014606,37.8215957 20.8604606,38.4695957 19.8664606,38.2235957 C19.9014606,38.2745957 17.3144606,37.5865957 17.3144606,37.5865957 L15.5714606,41.6055957 L20.1404606,42.7445957 C20.9904606,42.9575957 21.8234606,43.1805957 22.6434606,43.3905957 L21.1904606,49.2245957 L24.6974606,50.0995957 L26.1364606,44.3275957 C27.0944606,44.5875957 28.0244606,44.8275957 28.9344606,45.0535957 L27.5004606,50.7985957 L31.0114606,51.6735957 L32.4644606,45.8505957 C38.4514606,46.9835957 42.9534606,46.5265957 44.8484606,41.1115957 C46.3754606,36.7515957 44.7724606,34.2365957 41.6224606,32.5965957 C43.9164606,32.0675957 45.6444606,30.5585957 46.1054606,27.4415957 C46.7424606,23.1835957 43.5004606,20.8945957 39.0674606,19.3675957 Z M38.0834606,38.6905957 C36.9984606,43.0505957 29.6574606,40.6935957 27.2774606,40.1025957 L29.2054606,32.3735957 C31.5854606,32.9675957 39.2174606,34.1435957 38.0834606,38.6905957 Z M39.1694606,27.3785957 C38.1794606,31.3445957 32.0694606,29.3295957 30.0874606,28.8355957 L31.8354606,21.8255957 C33.8174606,22.3195957 40.2004606,23.2415957 39.1694606,27.3785957 Z' + ) bitcoinPath.setAttribute('fill', '#f7931a') // Orange color for Bitcoin - bitcoinPath.setAttribute('transform', `translate(${x - 45}, ${y - 50}) scale(1.4)`) // Scale and position the logo - + bitcoinPath.setAttribute( + 'transform', + `translate(${x - 45}, ${y - 50}) scale(1.4)` + ) // Scale and position the logo + logoGroup.appendChild(bitcoinPath) svg.appendChild(logoGroup) - + // Add "Incoming Payment" text above the Bitcoin logo - const incomingText = document.createElementNS('http://www.w3.org/2000/svg', 'text') + const incomingText = document.createElementNS( + 'http://www.w3.org/2000/svg', + 'text' + ) incomingText.setAttribute('x', x) incomingText.setAttribute('y', y - 50) incomingText.setAttribute('text-anchor', 'middle') incomingText.setAttribute('fill', isDarkTheme ? '#f9ca24' : '#f7931a') incomingText.textContent = 'Incoming Payment' - + svg.appendChild(incomingText) } }, - + template: `
` -}) \ No newline at end of file +}) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 8650fbe..b4568e6 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -16,9 +16,19 @@ header-nav :vertical="$q.screen.lt.md" > - + - + @@ -29,13 +39,17 @@ -
Select Source Wallet
-

Choose the wallet from which payments will be split.

- +
+ Select Source Wallet +
+

+ Choose the wallet from which payments will be split. +

+ - @@ -46,14 +60,19 @@ Split payments successfully activated!
- {% raw %}{{ lastSavedTargetCount }}{% endraw %} split target{% raw %}{{ lastSavedTargetCount !== 1 ? 's' : '' }}{% endraw %} configured for - {% raw %}{{ selectedWallet ? selectedWallet.name : 'the selected wallet' }}{% endraw %}. + {% raw %}{{ lastSavedTargetCount }}{% endraw %} split target{% raw + %}{{ lastSavedTargetCount !== 1 ? 's' : '' }}{% endraw %} configured + for + {% raw %}{{ selectedWallet ? selectedWallet.name : 'the selected + wallet' }}{% endraw %}.