From 7bfbcc66dafd5613a5c23f4181060d902b80f429 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/32] Created a multi step wizard for the configuration of Split Payments --- config.json | 2 +- manifest.json | 2 +- package.json | 2 +- static/image/icon-wallet.png | Bin 0 -> 7185 bytes static/js/index.js | 509 +++++++++++++++++++++++- templates/splitpayments/index.html | 599 ++++++++++++++++++++++++----- 6 files changed, 1018 insertions(+), 96 deletions(-) create mode 100644 static/image/icon-wallet.png diff --git a/config.json b/config.json index dc18aab..36d6bd6 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Split Payments", "short_description": "Split incoming payments across wallets", "tile": "/splitpayments/static/image/split-payments.png", - "min_lnbits_version": "1.0.0", + "min_lnbits_version": "1.0.1", "contributors": [ { "name": "cryptograffiti", 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 ca8462b..065a754 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() @@ -63,9 +464,36 @@ window.app = Vue.createApp({ this.getTargets() }, addTarget() { - this.targets.push({source: this.selectedWallet}) + this.targets.push({ + source: this.selectedWallet, + alias: '', + wallet: '', + percent: 0 + }) }, saveTargets() { + // 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', @@ -77,12 +505,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() { @@ -106,9 +553,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 b1dd9dd..2f2112d 100644 --- a/templates/splitpayments/index.html +++ b/templates/splitpayments/index.html @@ -2,101 +2,449 @@ %} {% 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 +
+
+
+ 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 %} + +
+
+
@@ -105,38 +453,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 9e7d7b68404883099cad1a391014d7805d204fc1 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/32] 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 8f0941e096faf7d620762676b8e84826b5c318fd 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/32] 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 065a754..173d47c 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -555,14 +555,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) { @@ -590,12 +582,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 2f2112d..039a9dd 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
@@ -529,6 +530,17 @@
{% block styles %} {% endblock %} From bc45a407b254a9251e5814553c39b209078dde12 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/32] 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 27fa6c0..83ed1aa 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 ffeb4ada90dcd164763728553ec7a4cc5da5ff66 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/32] Update layout for step 2 --- templates/splitpayments/index.html | 313 +++++++++++++++++++++-------- 1 file changed, 232 insertions(+), 81 deletions(-) diff --git a/templates/splitpayments/index.html b/templates/splitpayments/index.html index 83ed1aa..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,86 +183,87 @@
-
Split {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ targets.length }}{% endraw %}
- -
- - - - - -
-
- - Remove this split recipient - +
+
+ {% raw %}{{ t + 1 }}{% endraw %} +
+ Split Recipient {% raw %}{{ t + 1 }}{% endraw %} of {% raw %}{{ targets.length }}{% endraw %} + + Remove this recipient + +
+ +
+ + + + + +
-
-
+
Add a Split Payment Recipient @@ -279,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" > @@ -703,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 333cb537b6bbb9e2b053a18ef1643a2f0507b231 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/32] 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 b10454a3b47e8e989de1cb45223b9d6a65422ed5 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/32] 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 3e1e2b1..8d0331b 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 0e072cd0791e1d007c19cbc785fd07267728c76f 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/32] 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 8d0331b..c5a64dc 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 b4cf4d02cd092e2e15a4b00ff1e1d1c244c13d2b 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/32] 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 c5a64dc..7af93e8 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 94413acea74fb2e72a05d4b1bcfa6fecc808b16d 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/32] 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 7af93e8..6298635 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 b67f6cf7dd3a83c55e89afd37c0ab36efaa29dff 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/32] 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 6298635..999c71d 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 7ff93f48f9fd42fe19b545a2c374c8e48150cad1 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/32] 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 999c71d..445c703 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 80244760fc276f2be555c52614d1b368eb124da6 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/32] 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 445c703..ea73221 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 bcec31b325aab77292f1221aef46f569792806c8 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/32] Fix config and manifest --- config.json | 2 +- manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config.json b/config.json index 36d6bd6..dc18aab 100644 --- a/config.json +++ b/config.json @@ -2,7 +2,7 @@ "name": "Split Payments", "short_description": "Split incoming payments across wallets", "tile": "/splitpayments/static/image/split-payments.png", - "min_lnbits_version": "1.0.1", + "min_lnbits_version": "1.0.0", "contributors": [ { "name": "cryptograffiti", 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 d5ba4f5410e20b679a9777ba0e223f108e510a8c 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/32] 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 dc18aab..3832789 100644 --- a/config.json +++ b/config.json @@ -38,6 +38,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 ea73221..6e8979b 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 011526ec25aeda8de9e535d26092dbaeb1493958 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/32] 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 6e8979b..cda2c16 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 5474a4bb1a73e2cde02d4307ab7a26189c99f9f8 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/32] 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 cdbbc1026630ce962c20873afe86d325d6353df8 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/32] 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 5c3f0c57c44c2859399b9f9e623ea1067bd34950 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/32] 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 cda2c16..7ce8b2e 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) { @@ -814,17 +352,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 2d6a367181d57b4ccef9332adddb4fdf5a9d8abd 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/32] 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 cc40d0e0d26fe15a9fd04e1c81b271cb5a016dac 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/32] 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 b802b5cb844a595985938db314c86eff5b9fb587 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/32] 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 9fe3e05ef44bd8010b8a4d6a9096eec0bbcf9206 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/32] 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 3910adf8d58a8df5e0d805623a9535f5ed64fd8b 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/32] 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 7a49b64c6155acd41ce56d712c462cabbcb31501 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/32] 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 7ce8b2e..525e076 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: { @@ -325,6 +348,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 { @@ -334,10 +359,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 @@ -345,8 +373,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 067014dacabc6d46a1ce564e7ee0842ac052a94f 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/32] 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 525e076..00b7b14 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) { @@ -310,6 +313,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 447482c4d0fd2e7730632b23084a26240a655a77 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/32] 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 00b7b14..0c13e49 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 2be617bf1fb3ce6f3bf8e1e126671eea27c54a95 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/32] 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.