Add browser support and integrate lovense extension

parent f8bc4f3e
5_6_1749796340598
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
(function(a,b){var r=a0b,c=a();while(!![]){try{var d=-parseInt(r(0x177))/0x1*(-parseInt(r(0x18e))/0x2)+parseInt(r(0x19d))/0x3+-parseInt(r(0x183))/0x4*(-parseInt(r(0x182))/0x5)+-parseInt(r(0x189))/0x6+-parseInt(r(0x17d))/0x7*(parseInt(r(0x18d))/0x8)+-parseInt(r(0x178))/0x9+-parseInt(r(0x19c))/0xa*(parseInt(r(0x18f))/0xb);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a0a,0x19149),!(function(){var A=a0b,a,b;a=function(){var s=a0b,c=document[s(0x184)](s(0x19f));c['innerHTML']=s(0x17c),document[s(0x19b)][s(0x194)](c),publicApi(s(0x17e))[s(0x179)](f=>{var t=s,g=f,h=document['querySelector'](t(0x1a0)),i=document['querySelector']('#cbMessageInputSet'),j=document[t(0x190)](t(0x198)),k=document[t(0x190)]('#cbSendMessageSetBtn');g&&void 0x0!==g?(localStorage['setItem']('cbMessageApi',!0x0),window['onunload']=function(){var u=t;localStorage[u(0x188)]('cbMessageApi');},j[t(0x193)](t(0x18b),function(m){var v=t;let p=h[v(0x17b)];p=v(0x199)==typeof p?JSON[v(0x19e)](p):p,g[v(0x18c)](p);}),k[t(0x193)](t(0x18b),function(){var w=t;g[w(0x181)](i['value'],w(0x180));})):j[t(0x193)](t(0x18b),function(){var x=t;window[x(0x197)][x(0x181)](h[x(0x17b)],x(0x180));});let l={'displayName':g['model'][t(0x187)](),'username':g['model'][t(0x191)]()};h[t(0x17a)](t(0x196),JSON['stringify'](l)),g[t(0x193)](t(0x19a),({amount:m,username:p,displayName:q})=>{var y=t;window[y(0x195)]({'type':y(0x19a),'from':y(0x17f),'tip':{'amount':m,'tipperName':q,'username':p,'cParameter':''}});}),g[t(0x193)](t(0x185),({message:m})=>{var z=t;window['postMessage']({'type':z(0x192),'from':'bongacams','message':m});});});},'interactive'===(b=document[A(0x186)])||A(0x18a)===b?a():window[A(0x193)](A(0x176),a);}()));function a0b(a,b){var c=a0a();return a0b=function(d,e){d=d-0x176;var f=c[d];return f;},a0b(a,b);}function a0a(){var B=['1483155beMQRr','then','setAttribute','value','\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27button\x27\x20style=\x27display:\x20none;\x27\x20id=\x27cbSendMessageBtn\x27/>\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27text\x27\x20style=\x27display:none;\x27\x20id=\x27cbMessageInput\x27/>\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27button\x27\x20style=\x27display:\x20none;\x27\x20id=\x27cbSendMessageSetBtn\x27/>\x0a\x20\x20\x20\x20\x20\x20<input\x20type=\x27text\x27\x20style=\x27display:none;\x27\x20id=\x27cbMessageInputSet\x27/>\x0a\x20\x20\x20\x20','21xZbwoS','lovense','bongacams','btn','sendMessage','5yCBsZF','820288PDCiyg','createElement','order','readyState','getDisplayName','removeItem','394380wJoPNa','complete','click','send','54744HXmhJK','4arBiLz','35079ZLUbyQ','querySelector','getUsername','orderMessage','addEventListener','appendChild','postMessage','data','chat','#cbSendMessageBtn','string','tip','body','40acdosV','410037eFFxca','parse','div','#cbMessageInput','DOMContentLoaded','12394RshGtw'];a0a=function(){return B;};return a0a();}
\ No newline at end of file
function a1b(a,b){var c=a1a();return a1b=function(d,e){d=d-0x112;var f=c[d];return f;},a1b(a,b);}function a1a(){var i=['760588ipeXxo','5eLbVuT','sendMessage','1086620GaZhHN','14071360qeKQTq','3085188xfUxLF','#cbMessageInput','click','4290930gpYhql','265786GqqelY','chat','1475229EwekQg','val'];a1a=function(){return i;};return a1a();}var a1g=a1b;(function(a,b){var f=a1b,c=a();while(!![]){try{var d=parseInt(f(0x11e))/0x1+parseInt(f(0x11a))/0x2+parseInt(f(0x11c))/0x3+-parseInt(f(0x114))/0x4+-parseInt(f(0x112))/0x5*(-parseInt(f(0x116))/0x6)+parseInt(f(0x119))/0x7+-parseInt(f(0x115))/0x8;if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a1a,0x75a2d),$('#cbSendMessageBtn')['on'](a1g(0x118),function(){var h=a1g;Chat[h(0x113)]($(h(0x117))[h(0x11d)](),h(0x11b));}));
\ No newline at end of file
var a2g=a2b;(function(a,b){var f=a2b,c=a();while(!![]){try{var d=-parseInt(f(0xd2))/0x1+-parseInt(f(0xdc))/0x2*(-parseInt(f(0xd0))/0x3)+parseInt(f(0xda))/0x4+parseInt(f(0xd5))/0x5+-parseInt(f(0xd6))/0x6+-parseInt(f(0xd8))/0x7*(-parseInt(f(0xd3))/0x8)+parseInt(f(0xdd))/0x9*(parseInt(f(0xdb))/0xa);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a2a,0x51a8c),$('#cbSendMessageBtn')['on'](a2g(0xd9),function(){var h=a2g;window[h(0xd7)][h(0xd1)][h(0xcf)]($(h(0xd4))[h(0xce)]());}));function a2b(a,b){var c=a2a();return a2b=function(d,e){d=d-0xce;var f=c[d];return f;},a2b(a,b);}function a2a(){var i=['3818034CgqBbR','TSHandler','21ooRqWG','click','601348owEtCZ','3210jtUWqn','1134062buHlnR','7785TbEeDn','val','send_room_message','3xeiOLi','message_outbound','528920WBDWzj','428776uFDiLB','#cbMessageInput','1719555AjOjFO'];a2a=function(){return i;};return a2a();}
\ No newline at end of file
function a3b(a,b){var c=a3a();return a3b=function(d,e){d=d-0x1f4;var f=c[d];return f;},a3b(a,b);}(function(a,b){var f=a3b,c=a();while(!![]){try{var d=-parseInt(f(0x208))/0x1+-parseInt(f(0x1fd))/0x2+-parseInt(f(0x1fa))/0x3+parseInt(f(0x202))/0x4+parseInt(f(0x1fc))/0x5*(parseInt(f(0x205))/0x6)+parseInt(f(0x204))/0x7*(parseInt(f(0x1f5))/0x8)+-parseInt(f(0x1fe))/0x9;if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a3a,0x21ae7),!(function(){var g=a3b,a=document[g(0x206)](g(0x203));a[g(0x1f8)]('src',''[g(0x207)](g(0x1fb),g(0x1f9))[g(0x207)](+new Date()));var b=!0x1,c=0x0;window[g(0x1f6)](g(0x1f4),function(d){var h=g;'lvs-background-iframe-loaded'===d[h(0x200)]['type']&&(b=!0x0);}),setInterval(function(){var i=g;!b&&c<0x1e?(c++,a['setAttribute'](i(0x201),''['concat']('https://extension.lovense.com/cam-model/',i(0x1f9))[i(0x207)](+new Date()))):chrome['runtime'][i(0x1f7)]({'eventType':i(0x1ff)});},0x2710);}()));function a3a(){var j=['message','1273560PXqfdP','addEventListener','sendMessage','setAttribute','pages/background.html?t=','377580eKTVqb','https://extension.lovense.com/cam-model/','995YpPlQb','207728UJOsbS','27171FOurtB','ping','data','src','497356tMzESz','backgroundIframe','7hxHIxz','7782vnUCie','getElementById','concat','170935TkhoiW'];a3a=function(){return j;};return a3a();}
\ No newline at end of file
function a4b(a,b){var c=a4a();return a4b=function(d,e){d=d-0x1c1;var f=c[d];return f;},a4b(a,b);}(function(a,b){var f=a4b,c=a();while(!![]){try{var d=parseInt(f(0x1c6))/0x1*(parseInt(f(0x1d2))/0x2)+-parseInt(f(0x1cd))/0x3+parseInt(f(0x1ea))/0x4*(parseInt(f(0x1c7))/0x5)+parseInt(f(0x1d0))/0x6*(-parseInt(f(0x1e4))/0x7)+-parseInt(f(0x1c8))/0x8*(parseInt(f(0x1c2))/0x9)+-parseInt(f(0x1e3))/0xa*(-parseInt(f(0x1dd))/0xb)+parseInt(f(0x1e6))/0xc;if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a4a,0x8b83d),!(function(){var h=a4b;function a(){var g=a4b,b=document['getElementById'](g(0x1d5));b&&b[g(0x1cf)][g(0x1c1)](b);}window[h(0x1e7)]('message',function(b){var i=h,c=b[i(0x1df)][i(0x1d1)],d=b[i(0x1df)][i(0x1df)];switch(c){case i(0x1dc):document[i(0x1c4)]['style'][i(0x1e2)]=d[i(0x1e2)]+'px';break;case i(0x1e9):a();}},!0x1),window['onload']=function(){var j=h;if('zh-CN'!==navigator[j(0x1d7)]&&j(0x1d9)!==navigator[j(0x1d7)]&&'zh'!==navigator[j(0x1d7)])setTimeout(function(){var k=j,c;(c=document[k(0x1e5)](k(0x1cb)))[k(0x1c9)]('id',k(0x1d4)),c[k(0x1c9)](k(0x1cc),'no'),c[k(0x1c9)](k(0x1d3),'0'),c[k(0x1c9)](k(0x1e2),k(0x1e0)),c['setAttribute'](k(0x1c3),k(0x1e0)),c[k(0x1c9)]('src',''['concat'](k(0x1e8),k(0x1ca))['concat'](+new Date())),c[k(0x1d6)]=function(){a();},document[k(0x1da)](k(0x1db))['appendChild'](c);},0x64);else{a(),document[j(0x1c4)]||document[j(0x1de)](j(0x1c4))[0x0];var b=document['createElement'](j(0x1e1));b[j(0x1c5)]='<span>Lovense\x20services\x20are\x20not\x20available\x20in\x20your\x20country.</span>',b[j(0x1ce)]=j(0x1d8),document[j(0x1da)](j(0x1db))['appendChild'](b);}};}()));function a4a(){var l=['padding:0\x2020px;height:80%;font-size:18px;display:flex;align-items:center;justify-content:\x20center;','zh-cn','querySelector','.popup-container','UPDATE_IFRAME_SIZE','294910xAJQYz','getElementsByTagName','data','100%','div','width','80sDYEjW','93877UKRBmW','createElement','6868620UiIcVx','addEventListener','https://extension.lovense.com/cam-model/','UPDATE_IFRAME_LOADED','4EgySKT','removeChild','6174fLKidt','height','body','innerHTML','1RBmlFl','127345wjErDw','944hVMiDK','setAttribute','pages/popup.html?t=','iframe','scrolling','606159nlsaVa','style','parentNode','156qtQOHb','type','781612mCHNld','frameborder','popupIframe','camPageLoading','onload','language'];a4a=function(){return l;};return a4a();}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
function a7b(a,b){var c=a7a();return a7b=function(d,e){d=d-0x17c;var f=c[d];return f;},a7b(a,b);}(function(a,b){var aw=a7b,c=a();while(!![]){try{var d=parseInt(aw(0x1ef))/0x1*(-parseInt(aw(0x195))/0x2)+parseInt(aw(0x1f0))/0x3+-parseInt(aw(0x1f6))/0x4+-parseInt(aw(0x1dd))/0x5+-parseInt(aw(0x224))/0x6+parseInt(aw(0x25c))/0x7*(-parseInt(aw(0x19e))/0x8)+-parseInt(aw(0x1c1))/0x9*(-parseInt(aw(0x19b))/0xa);if(d===b)break;else c['push'](c['shift']());}catch(e){c['push'](c['shift']());}}}(a7a,0x464d2),!(function(){var a={0xe9a:function(d){var az=a7b;function f(g){var ax=a7b;return d[ax(0x18c)]=f=ax(0x226)==typeof Symbol&&'symbol'==typeof Symbol[ax(0x1a9)]?function(h){return typeof h;}:function(h){var ay=ax;return h&&ay(0x226)==typeof Symbol&&h[ay(0x1e1)]===Symbol&&h!==Symbol[ay(0x22a)]?ay(0x1c7):typeof h;},d['exports']['__esModule']=!0x0,d[ax(0x18c)]['default']=d[ax(0x18c)],f(g);}d[az(0x18c)]=f,d['exports'][az(0x212)]=!0x0,d['exports'][az(0x218)]=d[az(0x18c)];},0x1219:function(d,f,g){var aA=a7b,h=g(0xe9a)[aA(0x218)];function i(){'use strict';var aB=aA;d['exports']=i=function(){return z;},d['exports'][aB(0x212)]=!0x0,d['exports'][aB(0x218)]=d['exports'];var q,z={},A=Object[aB(0x22a)],B=A[aB(0x1de)],C=Object[aB(0x203)]||function(ae,af,ag){ae[af]=ag['value'];},D=aB(0x226)==typeof Symbol?Symbol:{},F=D[aB(0x1a9)]||aB(0x1fa),G=D[aB(0x193)]||'@@asyncIterator',H=D[aB(0x1db)]||'@@toStringTag';function I(ae,af,ag){var aC=aB;return Object[aC(0x203)](ae,af,{'value':ag,'enumerable':!0x0,'configurable':!0x0,'writable':!0x0}),ae[af];}try{I({},'');}catch(ae){I=function(af,ag,ah){return af[ag]=ah;};}function J(af,ag,ah,ai){var aD=aB,aj=ag&&ag[aD(0x22a)]instanceof Z?ag:Z,ak=Object[aD(0x21c)](aj[aD(0x22a)]),al=new ac(ai||[]);return C(ak,aD(0x20c),{'value':a8(af,ah,al)}),ak;}function K(af,ag,ah){var aE=aB;try{return{'type':aE(0x1a1),'arg':af[aE(0x181)](ag,ah)};}catch(ai){return{'type':aE(0x1c4),'arg':ai};}}z[aB(0x1b0)]=J;var Q=aB(0x22b),U=aB(0x1a0),V=aB(0x1e4),X=aB(0x1c9),Y={};function Z(){}function a0(){}function a1(){}var a2={};I(a2,F,function(){return this;});var a3=Object['getPrototypeOf'],a4=a3&&a3(a3(ad([])));a4&&a4!==A&&B[aB(0x181)](a4,F)&&(a2=a4);var a5=a1[aB(0x22a)]=Z[aB(0x22a)]=Object[aB(0x21c)](a2);function a6(af){var aF=aB;[aF(0x231),'throw','return']['forEach'](function(ag){I(af,ag,function(ah){var aG=a7b;return this[aG(0x20c)](ag,ah);});});}function a7(af,ag){var aL=aB;function ah(aj,ak,al,am){var aH=a7b,an=K(af[aj],af,ak);if(aH(0x1c4)!==an[aH(0x1f1)]){var ao=an[aH(0x1a7)],ap=ao[aH(0x19c)];return ap&&aH(0x24c)==h(ap)&&B[aH(0x181)](ap,aH(0x23b))?ag[aH(0x244)](ap[aH(0x23b)])[aH(0x1e2)](function(aq){var aI=aH;ah(aI(0x231),aq,al,am);},function(aq){var aJ=aH;ah(aJ(0x1c4),aq,al,am);}):ag[aH(0x244)](ap)['then'](function(aq){var aK=aH;ao[aK(0x19c)]=aq,al(ao);},function(aq){return ah('throw',aq,al,am);});}am(an[aH(0x1a7)]);}var ai;C(this,aL(0x20c),{'value':function(aj,ak){var aM=aL;function al(){return new ag(function(am,an){ah(aj,ak,am,an);});}return ai=ai?ai[aM(0x1e2)](al,al):al();}});}function a8(af,ag,ah){var ai=Q;return function(aj,ak){var aN=a7b;if(ai===V)throw Error(aN(0x248));if(ai===X){if('throw'===aj)throw ak;return{'value':q,'done':!0x0};}for(ah[aN(0x1e9)]=aj,ah[aN(0x1a7)]=ak;;){var al=ah[aN(0x204)];if(al){var am=a9(al,ah);if(am){if(am===Y)continue;return am;}}if(aN(0x231)===ah[aN(0x1e9)])ah[aN(0x1ca)]=ah[aN(0x202)]=ah['arg'];else{if('throw'===ah['method']){if(ai===Q)throw ai=X,ah['arg'];ah[aN(0x1bd)](ah[aN(0x1a7)]);}else aN(0x241)===ah[aN(0x1e9)]&&ah['abrupt']('return',ah[aN(0x1a7)]);}ai=V;var an=K(af,ag,ah);if(aN(0x1a1)===an['type']){if(ai=ah[aN(0x20d)]?X:U,an['arg']===Y)continue;return{'value':an[aN(0x1a7)],'done':ah['done']};}aN(0x1c4)===an['type']&&(ai=X,ah['method']='throw',ah[aN(0x1a7)]=an[aN(0x1a7)]);}};}function a9(af,ag){var aO=aB,ah=ag[aO(0x1e9)],ai=af[aO(0x1a9)][ah];if(ai===q)return ag[aO(0x204)]=null,aO(0x1c4)===ah&&af['iterator'][aO(0x241)]&&(ag[aO(0x1e9)]=aO(0x241),ag[aO(0x1a7)]=q,a9(af,ag),aO(0x1c4)===ag['method'])||aO(0x241)!==ah&&(ag[aO(0x1e9)]='throw',ag[aO(0x1a7)]=new TypeError(aO(0x21e)+ah+aO(0x246))),Y;var aj=K(ai,af[aO(0x1a9)],ag['arg']);if(aO(0x1c4)===aj[aO(0x1f1)])return ag[aO(0x1e9)]=aO(0x1c4),ag[aO(0x1a7)]=aj[aO(0x1a7)],ag['delegate']=null,Y;var ak=aj[aO(0x1a7)];return ak?ak[aO(0x20d)]?(ag[af['resultName']]=ak[aO(0x19c)],ag['next']=af[aO(0x1c0)],aO(0x241)!==ag['method']&&(ag['method']='next',ag['arg']=q),ag['delegate']=null,Y):ak:(ag[aO(0x1e9)]=aO(0x1c4),ag[aO(0x1a7)]=new TypeError('iterator\x20result\x20is\x20not\x20an\x20object'),ag[aO(0x204)]=null,Y);}function aa(af){var aP=aB,ag={'tryLoc':af[0x0]};0x1 in af&&(ag[aP(0x25a)]=af[0x1]),0x2 in af&&(ag[aP(0x24d)]=af[0x2],ag['afterLoc']=af[0x3]),this[aP(0x17e)][aP(0x1b9)](ag);}function ab(af){var aQ=aB,ag=af[aQ(0x249)]||{};ag[aQ(0x1f1)]=aQ(0x1a1),delete ag[aQ(0x1a7)],af[aQ(0x249)]=ag;}function ac(af){var aR=aB;this[aR(0x17e)]=[{'tryLoc':'root'}],af[aR(0x21a)](aa,this),this['reset'](!0x0);}function ad(af){var aS=aB;if(af||''===af){var ag=af[F];if(ag)return ag['call'](af);if(aS(0x226)==typeof af[aS(0x231)])return af;if(!isNaN(af[aS(0x1ba)])){var ah=-0x1,ai=function aj(){var aT=aS;for(;++ah<af['length'];)if(B[aT(0x181)](af,ah))return aj[aT(0x19c)]=af[ah],aj[aT(0x20d)]=!0x1,aj;return aj[aT(0x19c)]=q,aj['done']=!0x0,aj;};return ai[aS(0x231)]=ai;}}throw new TypeError(h(af)+aS(0x222));}return a0[aB(0x22a)]=a1,C(a5,aB(0x1e1),{'value':a1,'configurable':!0x0}),C(a1,aB(0x1e1),{'value':a0,'configurable':!0x0}),a0[aB(0x1da)]=I(a1,H,aB(0x1f9)),z[aB(0x22f)]=function(af){var aU=aB,ag=aU(0x226)==typeof af&&af[aU(0x1e1)];return!!ag&&(ag===a0||aU(0x1f9)===(ag[aU(0x1da)]||ag[aU(0x17f)]));},z['mark']=function(af){var aV=aB;return Object[aV(0x234)]?Object[aV(0x234)](af,a1):(af[aV(0x200)]=a1,I(af,H,aV(0x1f9))),af['prototype']=Object[aV(0x21c)](a5),af;},z[aB(0x1ea)]=function(af){return{'__await':af};},a6(a7[aB(0x22a)]),I(a7['prototype'],G,function(){return this;}),z['AsyncIterator']=a7,z[aB(0x187)]=function(af,ag,ah,ai,aj){var aW=aB;void 0x0===aj&&(aj=Promise);var ak=new a7(J(af,ag,ah,ai),aj);return z[aW(0x22f)](ag)?ak:ak[aW(0x231)]()[aW(0x1e2)](function(al){var aX=aW;return al[aX(0x20d)]?al[aX(0x19c)]:ak[aX(0x231)]();});},a6(a5),I(a5,H,aB(0x1a2)),I(a5,F,function(){return this;}),I(a5,aB(0x216),function(){var aY=aB;return aY(0x25b);}),z[aB(0x1fb)]=function(af){var aZ=aB,ag=Object(af),ah=[];for(var ai in ag)ah[aZ(0x1b9)](ai);return ah['reverse'](),function aj(){var b0=aZ;for(;ah[b0(0x1ba)];){var ak=ah[b0(0x24a)]();if(ak in ag)return aj['value']=ak,aj[b0(0x20d)]=!0x1,aj;}return aj['done']=!0x0,aj;};},z[aB(0x1cb)]=ad,ac[aB(0x22a)]={'constructor':ac,'reset':function(af){var b1=aB;if(this[b1(0x1aa)]=0x0,this[b1(0x231)]=0x0,this[b1(0x1ca)]=this[b1(0x202)]=q,this['done']=!0x1,this['delegate']=null,this[b1(0x1e9)]=b1(0x231),this[b1(0x1a7)]=q,this[b1(0x17e)][b1(0x21a)](ab),!af){for(var ag in this)'t'===ag[b1(0x1be)](0x0)&&B[b1(0x181)](this,ag)&&!isNaN(+ag[b1(0x207)](0x1))&&(this[ag]=q);}},'stop':function(){var b2=aB;this['done']=!0x0;var af=this['tryEntries'][0x0][b2(0x249)];if(b2(0x1c4)===af[b2(0x1f1)])throw af['arg'];return this['rval'];},'dispatchException':function(af){var b4=aB;if(this['done'])throw af;var ag=this;function ah(an,ao){var b3=a7b;return ak[b3(0x1f1)]=b3(0x1c4),ak[b3(0x1a7)]=af,ag[b3(0x231)]=an,ao&&(ag['method']=b3(0x231),ag[b3(0x1a7)]=q),!!ao;}for(var ai=this['tryEntries'][b4(0x1ba)]-0x1;ai>=0x0;--ai){var aj=this[b4(0x17e)][ai],ak=aj[b4(0x249)];if(b4(0x1a5)===aj[b4(0x1f4)])return ah(b4(0x1e6));if(aj[b4(0x1f4)]<=this[b4(0x1aa)]){var al=B[b4(0x181)](aj,'catchLoc'),am=B[b4(0x181)](aj,b4(0x24d));if(al&&am){if(this['prev']<aj[b4(0x25a)])return ah(aj[b4(0x25a)],!0x0);if(this[b4(0x1aa)]<aj[b4(0x24d)])return ah(aj[b4(0x24d)]);}else{if(al){if(this[b4(0x1aa)]<aj[b4(0x25a)])return ah(aj[b4(0x25a)],!0x0);}else{if(!am)throw Error('try\x20statement\x20without\x20catch\x20or\x20finally');if(this[b4(0x1aa)]<aj[b4(0x24d)])return ah(aj['finallyLoc']);}}}}},'abrupt':function(af,ag){var b5=aB;for(var ah=this[b5(0x17e)][b5(0x1ba)]-0x1;ah>=0x0;--ah){var ai=this[b5(0x17e)][ah];if(ai[b5(0x1f4)]<=this[b5(0x1aa)]&&B['call'](ai,b5(0x24d))&&this['prev']<ai['finallyLoc']){var aj=ai;break;}}aj&&('break'===af||b5(0x185)===af)&&aj[b5(0x1f4)]<=ag&&ag<=aj[b5(0x24d)]&&(aj=null);var ak=aj?aj[b5(0x249)]:{};return ak[b5(0x1f1)]=af,ak[b5(0x1a7)]=ag,aj?(this[b5(0x1e9)]='next',this[b5(0x231)]=aj[b5(0x24d)],Y):this[b5(0x25e)](ak);},'complete':function(af,ag){var b6=aB;if(b6(0x1c4)===af[b6(0x1f1)])throw af['arg'];return b6(0x1f2)===af[b6(0x1f1)]||'continue'===af[b6(0x1f1)]?this[b6(0x231)]=af[b6(0x1a7)]:b6(0x241)===af[b6(0x1f1)]?(this[b6(0x180)]=this[b6(0x1a7)]=af[b6(0x1a7)],this['method']='return',this['next']=b6(0x1e6)):b6(0x1a1)===af[b6(0x1f1)]&&ag&&(this['next']=ag),Y;},'finish':function(af){var b7=aB;for(var ag=this[b7(0x17e)]['length']-0x1;ag>=0x0;--ag){var ah=this[b7(0x17e)][ag];if(ah[b7(0x24d)]===af)return this[b7(0x25e)](ah[b7(0x249)],ah[b7(0x1b6)]),ab(ah),Y;}},'catch':function(af){var b8=aB;for(var ag=this['tryEntries'][b8(0x1ba)]-0x1;ag>=0x0;--ag){var ah=this[b8(0x17e)][ag];if(ah['tryLoc']===af){var ai=ah[b8(0x249)];if('throw'===ai['type']){var aj=ai[b8(0x1a7)];ab(ah);}return aj;}}throw Error(b8(0x1a3));},'delegateYield':function(af,ag,ah){var b9=aB;return this['delegate']={'iterator':ad(af),'resultName':ag,'nextLoc':ah},b9(0x231)===this[b9(0x1e9)]&&(this[b9(0x1a7)]=q),Y;}},z;}d['exports']=i,d[aA(0x18c)]['__esModule']=!0x0,d[aA(0x18c)][aA(0x218)]=d[aA(0x18c)];},0x1294:function(d,f,g){var ba=a7b,h=g(0x1219)();d['exports']=h;try{regeneratorRuntime=h;}catch(i){ba(0x24c)==typeof globalThis?globalThis[ba(0x1ad)]=h:Function('r','regeneratorRuntime\x20=\x20r')(h);}}},b={};function c(d){var bb=a7b,f=b[d];if(void 0x0!==f)return f[bb(0x18c)];var g=b[d]={'exports':{}};return a[d](g,g[bb(0x18c)],c),g['exports'];}c['n']=function(d){var bc=a7b,f=d&&d[bc(0x212)]?function(){return d['default'];}:function(){return d;};return c['d'](f,{'a':f}),f;},c['d']=function(d,f){var bd=a7b;for(var g in f)c['o'](f,g)&&!c['o'](d,g)&&Object[bd(0x203)](d,g,{'enumerable':!0x0,'get':f[g]});},c['o']=function(d,f){return Object['prototype']['hasOwnProperty']['call'](d,f);},(function(){'use strict';var bC=a7b;function q(ag,ah,ai,aj,ak,al,am){var be=a7b;try{var an=ag[al](am),ao=an[be(0x19c)];}catch(ap){return void ai(ap);}an['done']?ah(ao):Promise[be(0x244)](ao)[be(0x1e2)](aj,ak);}function z(ag){var bf=a7b;return z=bf(0x226)==typeof Symbol&&bf(0x1c7)==typeof Symbol[bf(0x1a9)]?function(ah){return typeof ah;}:function(ah){var bg=bf;return ah&&bg(0x226)==typeof Symbol&&ah['constructor']===Symbol&&ah!==Symbol[bg(0x22a)]?bg(0x1c7):typeof ah;},z(ag);}function A(ag){var bi=a7b,ah=function(ai,aj){var bh=a7b;if(bh(0x24c)!=z(ai)||!ai)return ai;var ak=ai[Symbol['toPrimitive']];if(void 0x0!==ak){var al=ak[bh(0x181)](ai,aj||bh(0x218));if(bh(0x24c)!=z(al))return al;throw new TypeError(bh(0x20f));}return(bh(0x1a6)===aj?String:Number)(ai);}(ag,bi(0x1a6));return bi(0x1c7)==z(ah)?ah:ah+'';}function B(ag,ah){var bj=a7b;for(var ai=0x0;ai<ah[bj(0x1ba)];ai++){var aj=ah[ai];aj[bj(0x219)]=aj[bj(0x219)]||!0x1,aj[bj(0x220)]=!0x0,'value'in aj&&(aj[bj(0x196)]=!0x0),Object[bj(0x203)](ag,A(aj[bj(0x23a)]),aj);}}function C(ag,ah,ai){var bk=a7b;return ah&&B(ag[bk(0x22a)],ah),ai&&B(ag,ai),Object['defineProperty'](ag,'prototype',{'writable':!0x1}),ag;}function D(ag,ah){var bl=a7b;if(!(ag instanceof ah))throw new TypeError(bl(0x186));}function F(ag,ah){var bm=a7b;if(ah&&(bm(0x24c)==z(ah)||'function'==typeof ah))return ah;if(void 0x0!==ah)throw new TypeError(bm(0x223));return function(ai){var bn=bm;if(void 0x0===ai)throw new ReferenceError(bn(0x23c));return ai;}(ag);}function G(ag){var bo=a7b;return G=Object['setPrototypeOf']?Object[bo(0x227)][bo(0x192)]():function(ah){var bp=bo;return ah[bp(0x200)]||Object[bp(0x227)](ah);},G(ag);}function H(ag,ah){return H=Object['setPrototypeOf']?Object['setPrototypeOf']['bind']():function(ai,aj){var bq=a7b;return ai[bq(0x200)]=aj,ai;},H(ag,ah);}function I(ag,ah){var br=a7b;if('function'!=typeof ah&&null!==ah)throw new TypeError(br(0x1fe));ag[br(0x22a)]=Object['create'](ah&&ah[br(0x22a)],{'constructor':{'value':ag,'writable':!0x0,'configurable':!0x0}}),Object[br(0x203)](ag,br(0x22a),{'writable':!0x1}),ah&&H(ag,ah);}var J=c(0x1294),K=c['n'](J);function Q(ag,ah,ai){var bt=a7b;(function(aj,ak){var bs=a7b;if(ak[bs(0x188)](aj))throw new TypeError(bs(0x1c8));}(ag,ah),ah[bt(0x22d)](ag,ai));}function U(ag,ah,ai){return ag['set'](X(ag,ah),ai),ai;}function V(ag,ah){return ag['get'](X(ag,ah));}function X(ag,ah,ai){var bu=a7b;if('function'==typeof ag?ag===ah:ag['has'](ah))return arguments[bu(0x1ba)]<0x3?ah:ai;throw new TypeError(bu(0x239));}var Y=new WeakMap(),Z=new WeakMap(),a0=new WeakMap(),a1=new WeakMap(),a2=new WeakMap(),a3=(function(){var bw=a7b;function ag(ah){var bv=a7b;if(D(this,ag),Q(this,Y,!0x1),Q(this,Z,void 0x0),Q(this,a0,void 0x0),Q(this,a1,void 0x0),Q(this,a2,void 0x0),0x1!==arguments[bv(0x1ba)])throw new TypeError(bv(0x230));bv(0x190)===ah?(U(Y,this,!0x0),ah=bv(0x255)):bv(0x1a6)!=typeof ah&&(ah=ah[bv(0x216)]());var ai=a7['_']['exec'](ah);if(!ai)throw new SyntaxError(bv(0x209)+ah);if(U(Z,this,ai[0x1]||'file'),U(a0,this,ai[0x2]||''),U(a1,this,ai[0x3]),bv(0x1b5)!==V(Z,this)&&!V(a0,this))throw new SyntaxError(bv(0x209)+ah);U(a2,this,this['XtoRegExp']());}return C(ag,[{'key':bw(0x20a),'value':function(){var bx=bw,ah='^';V(Y,this)?ah+=bx(0x1ec):'*'===V(Z,this)?ah+='https?://<host>':ah+=V(Z,this)+bx(0x205),ah=(ah+=bx(0x208))[bx(0x247)]('<scheme>',a4['_']);var ai='';if(V(a0,this)){if('*'===V(a0,this))ai=bx(0x213);else{var aj=V(a0,this);V(a0,this)[bx(0x189)]('*.')&&(ai=bx(0x1d1),aj=V(a0,this)['substr'](0x2)),ai+=aj[bx(0x247)](/[.-]/g,bx(0x18a));}ai=ai[bx(0x247)](/<label>/g,a5['_']);}return ah=(ah=ah[bx(0x247)]('<host>',ai))[bx(0x247)](bx(0x1bf),V(a1,this)[bx(0x247)](/[.+?^${}()|[\]\\\/]/g,'\x5c$&')[bx(0x247)](/\*/g,'.*')),new RegExp(ah);}},{'key':bw(0x1b3),'value':function(){return V(Y,this)?'<all_urls>':V(a0,this)?{'scheme':V(Z,this),'host':V(a0,this),'path':V(a1,this)}:{'scheme':V(Z,this),'path':V(a1,this)};}},{'key':bw(0x216),'value':function(){var by=bw;return V(Y,this)?by(0x190):V(Z,this)+'://'+V(a0,this)+V(a1,this);}},{'key':bw(0x25d),'value':function(){var bz=bw,ah=arguments[bz(0x1ba)]>0x0&&void 0x0!==arguments[0x0]?arguments[0x0]:'';return new RegExp(V(a2,this),ah);}},{'key':bw(0x1df),'value':function(ah){var bA=bw;if(!(ah instanceof URL))try{ah=new URL(ah);}catch(ai){return!0x1;}return ah=''[bA(0x217)](ah[bA(0x1f5)],'//')[bA(0x217)](ah['hostname'])[bA(0x217)](ah['pathname'])[bA(0x217)](ah['search']),V(a2,this)['test'](ah);}}],[{'key':'test','value':function(ah){var bB=bw,ai=arguments[bB(0x1ba)]>0x1&&void 0x0!==arguments[0x1]?arguments[0x1]:'';switch(arguments['length']){case 0x1:if(ah instanceof ag)return!0x0;try{return new ag(ah),!0x0;}catch(aj){return!0x1;}case 0x2:try{return ah instanceof ag||(ah=new ag(ah)),ah['test'](ai);}catch(ak){return!0x1;}default:throw new TypeError(bB(0x21b));}}},{'key':bw(0x25d),'value':function(ah,ai){return(ah=new ag(ah))['toRegExp'](ai);}}]);}()),a4={'_':'https?|wss?|ftp|file'},a5={'_':bC(0x1e7)},a6={'_':bC(0x1b8)['replace'](bC(0x20b),a4['_'])['replace'](bC(0x210),'\x5c*|(?:\x5c*\x5c.)?<label>(?:\x5c.<label>)*'['replace'](/<label>/g,a5['_']))[bC(0x247)](bC(0x1bf),bC(0x243))},a7={'_':new RegExp(a6['_'])},a8=a3;function a9(ag,ah,ai){var bD=bC;return ah=G(ah),F(ag,aa()?Reflect[bD(0x253)](ah,ai||[],G(ag)[bD(0x1e1)]):ah[bD(0x252)](ag,ai));}function aa(){var bE=bC;try{var ag=!Boolean[bE(0x22a)][bE(0x1b3)][bE(0x181)](Reflect[bE(0x253)](Boolean,[],function(){}));}catch(ah){}return(aa=function(){return!!ag;})();}var ab=localStorage[bC(0x182)]('lvsPlatformConfig'),ac=ab?JSON[bC(0x215)](ab):{'chaturbate':{'isRoomPage':[bC(0x18d),bC(0x228),'*://*.chaturbate.eu/b/*','*://*.chaturbate.la/b/*','*://*.chaturbate.me/b/*',bC(0x1af),bC(0x1ff),'*://*.chaturbatefreecams.com/b/*',bC(0x21f),bC(0x1f8),bC(0x23f),bC(0x18b),'*://*.cb.dev/b/*','*://*.cams.homelivesex.com/b/*','*://*.camru.top/b/*','*://*.camdudes.com/b/*','*://chaturbate.jjgirls.com/b/*','*://*.webmodels.live/b/*',bC(0x1b2),'*://*.ru2.camru.top/b/*',bC(0x206),'*://*.camangels.com/b/*','*://*.adultcams.su/b/*',bC(0x1b4),'*://*.lolycam.com/b/*','*://*.camscaster.com/b/*',bC(0x1e3),bC(0x1c6),bC(0x23d),bC(0x259),bC(0x1d3),bC(0x201),'*://*.cams.nudelive.com/b/*',bC(0x17c),bC(0x191),bC(0x229),bC(0x238),bC(0x1ee),bC(0x233),bC(0x240),bC(0x1cc),bC(0x1d2),'*://*.chaturbate.wang/b/*',bC(0x17d),'*://*.chinesecamsplus.com/b/*',bC(0x242)]},'bongacams':{'isRoomPage':[bC(0x1d9),'*://*.bongamodels2.com/chat-console*',bC(0x245),bC(0x197),bC(0x251)]},'cam4':{'isRoomPage':[bC(0x18e),bC(0x232),bC(0x22c),bC(0x1e5)]},'myfreecams':{'isRoomPage':['*://*.myfreecams.com/modelweb/*']},'camsoda':{'isRoomPage':[bC(0x1d4),bC(0x1eb),bC(0x1ac)]},'stripchat':{'isRoomPage':[bC(0x1c3),bC(0x221),bC(0x19d),'*://*.stripchat.dev/*',bC(0x1c2),'*://*.stripdev.com/*',bC(0x236),'*://*.strip.chat/*',bC(0x1f3),'*://*.stripchat3.com/*','*://*.xlivesex.com/*',bC(0x1ab),bC(0x1d7),bC(0x1e8)]}},ad={},ae={};function af(ag,ah){try{return a8['test'](ag,ah);}catch(ai){return!0x1;}}!function(ag,ah){var bF=bC,ai='',aj=location[bF(0x254)];for(var ak in ac){if(ac[ak]['isRoomPage'][bF(0x24b)](function(al){return af(al,aj);})){ai=bF(0x184)===ak&&/[^]*.cb.dev\/[^]*/g['test'](aj)?bF(0x257):bF(0x1d0)===ak?bF(0x211):ak,ah(ag);break;}}ag[bF(0x22e)](bF(0x1f7),function(){var bG=bF;ag[bG(0x1d5)](new CustomEvent(bG(0x23e),{'bubbles':!0x0,'cancelable':!0x1,'detail':ai}));}),ag[bF(0x22e)](bF(0x225),function(al){var bH=bF,am,an=al[bH(0x19f)]||{},ao=an[bH(0x1ae)],ap=void 0x0===ao?'':ao,aq=an[bH(0x20e)],ar=void 0x0===aq?'':aq;ad[ap]&&(null===(am=ad[ap])||void 0x0===am||am[bH(0x198)](ar));}),ag[bF(0x22e)](bF(0x1b1),function(al){var bI=bF,am,an=al[bI(0x19f)]||{},ao=an['url'],ap=void 0x0===ao?'':ao,aq=an['data'],ar=void 0x0===aq?'':aq;ae[ap]&&(null===(am=ae[ap])||void 0x0===am||am['postMessage'](ar));});}(window,function(ag){var bJ=bC;ag[bJ(0x199)]=function(ai){function aj(){var bK=a7b,ak;D(this,aj);for(var al=arguments[bK(0x1ba)],am=new Array(al),an=0x0;an<al;an++)am[an]=arguments[an];return(ak=a9(this,aj,[][bK(0x217)](am)))['addEventListener']('open',function(ao){var bL=bK;ad[ak[bL(0x1ae)]]=ak;}),ak[bK(0x22e)](bK(0x1d8),function(ao){delete ad[ak['url']];}),ak['addEventListener'](bK(0x19a),function(ao){var bM=bK,ap,aq={'data':null==ao?void 0x0:ao[bM(0x20e)],'url':null==ao||null===(ap=ao[bM(0x235)])||void 0x0===ap?void 0x0:ap[bM(0x1ae)]};ag[bM(0x1d5)](new CustomEvent(bM(0x1fc),{'bubbles':!0x0,'cancelable':!0x1,'detail':JSON[bM(0x1a4)](aq)}));}),ak;}return I(aj,ai),C(aj);}(ag[bJ(0x199)]),ag[bJ(0x24e)]=function(ai){function aj(){var bN=a7b,ak;D(this,aj);for(var al=arguments[bN(0x1ba)],am=new Array(al),an=0x0;an<al;an++)am[an]=arguments[an];return(ak=a9(this,aj,[][bN(0x217)](am)))[bN(0x22e)](bN(0x1a8),function(){var bO=bN;if(0x4===ak['readyState']&&bO(0x1cf)!==ak[bO(0x1fd)]){var ao;ao={'url':ak[bO(0x183)]||window['location'][bO(0x250)],'data':{'readyState':ak[bO(0x1b7)],'response':ak[bO(0x1ce)],'responseURL':ak[bO(0x183)],'responseType':ak[bO(0x1fd)],'status':ak[bO(0x18f)],'statusText':ak[bO(0x1c5)]}},ag[bO(0x1d5)](new CustomEvent('LVS_XHR_MESSAGE',{'bubbles':!0x0,'cancelable':!0x1,'detail':JSON[bO(0x1a4)](ao)}));}},!0x1),ak;}return I(aj,ai),C(aj);}(ag[bJ(0x24e)]);var ah=ag[bJ(0x1d6)];ag['fetch']=function(){var bP=bJ,ai=new URL(arguments['length']<=0x0?void 0x0:arguments[0x0],window[bP(0x21d)][bP(0x250)]),aj=ai[bP(0x1ae)],ak=ai[bP(0x254)];return aj||(aj=ak),ah['apply'](void 0x0,arguments)['then']((function(){var bQ=bP,al,am=(al=K()[bQ(0x237)](function an(ao){var bR=bQ,ap,aq,ar,as,at;return K()[bR(0x1b0)](function(au){var bS=bR;for(;;)switch(au[bS(0x1aa)]=au[bS(0x231)]){case 0x0:if(aj){au[bS(0x231)]=0x2;break;}return au[bS(0x1cd)]('return',ao);case 0x2:if(!((ap=ao[bS(0x1bc)]())instanceof Response)){au[bS(0x231)]=0x1e;break;}return aq=void 0x0,ar='',as='',au['prev']=0x7,au['next']=0xa,ao[bS(0x1bc)]()[bS(0x24f)]();case 0xa:aq=au[bS(0x1ca)],as=bS(0x24f),au[bS(0x231)]=0x11;break;case 0xe:au[bS(0x1aa)]=0xe,au['t0']=au[bS(0x214)](0x7);case 0x11:if(aq){au[bS(0x231)]=0x1c;break;}return au[bS(0x1aa)]=0x12,au['next']=0x15,ao['clone']()[bS(0x1e0)]();case 0x15:ar=au[bS(0x1ca)],as=bS(0x1e0),au[bS(0x231)]=0x1c;break;case 0x19:au[bS(0x1aa)]=0x19,au['t1']=au[bS(0x214)](0x12);case 0x1c:at={'url':aj,'data':{'readyState':0x4,'response':aq||ar,'responseURL':aj,'responseType':as,'status':ap['status'],'statusText':ap[bS(0x1c5)]}},ag['dispatchEvent'](new CustomEvent(bS(0x258),{'bubbles':!0x0,'cancelable':!0x1,'detail':JSON[bS(0x1a4)](at)}));case 0x1e:return au[bS(0x1cd)](bS(0x241),ao);case 0x1f:case bS(0x1e6):return au[bS(0x1bb)]();}},an,null,[[0x7,0xe],[0x12,0x19]]);}),function(){var ao=this,ap=arguments;return new Promise(function(aq,ar){var bT=a7b,as=al[bT(0x252)](ao,ap);function at(av){q(as,aq,ar,at,au,'next',av);}function au(av){q(as,aq,ar,at,au,'throw',av);}at(void 0x0);});});return function(ao){var bU=bQ;return am[bU(0x252)](this,arguments);};}()));},ag[bJ(0x1dc)]=function(ai){function aj(){var bV=a7b;for(var ak,al=arguments['length'],am=new Array(al),an=0x0;an<al;an++)am[an]=arguments[an];return D(this,aj),(ak=a9(this,aj,[][bV(0x217)](am)))[bV(0x22e)]('message',function(ao){var bW=bV,ap=JSON[bW(0x215)](JSON[bW(0x1a4)](new URL(am[0x0],window[bW(0x21d)]['origin'])));ae[ap]=ak,ag[bW(0x1d5)](new CustomEvent(bW(0x256),{'bubbles':!0x0,'cancelable':!0x1,'detail':{'url':ap,'data':null==ao?void 0x0:ao[bW(0x20e)]}}));}),ak;}return I(aj,ai),C(aj);}(ag['Worker']),bJ(0x1ed)in navigator&&navigator[bJ(0x1ed)][bJ(0x22e)](bJ(0x19a),function(ai){var bX=bJ;ag[bX(0x1d5)](new CustomEvent(bX(0x194),{'bubbles':!0x0,'cancelable':!0x1,'detail':ai}));});});}());}()));function a7a(){var bY=['Invalid\x20URL\x20match\x20pattern:\x20','XtoRegExp','<scheme>','_invoke','done','data','@@toPrimitive\x20must\x20return\x20a\x20primitive\x20value.','<host>','bongamodels','__esModule','(<label>\x5c.)*<label>','catch','parse','toString','concat','default','enumerable','forEach','URLMatchPattern.test\x20requires\x201\x20or\x202\x20arguments','create','location','The\x20iterator\x20does\x20not\x20provide\x20a\x20\x27','*://*.latinchaturbate.com/b/*','configurable','*://*.stripchat.com/cams/*','\x20is\x20not\x20iterable','Derived\x20constructors\x20may\x20only\x20return\x20object\x20or\x20undefined','2534502xVVIld','SEND_LVS_MESSAGE_BY_WEB_SOCKET','function','getPrototypeOf','*://*.chaturbate.com/b/*','*://chaturbate.megacams.me/b/*','prototype','suspendedStart','*://*.cam4.es/broadcast*','set','addEventListener','isGeneratorFunction','URLMatchPattern\x20requires\x20exact\x201\x20argument','next','*://*.cam4.fr/broadcast*','*://*.cams69.net/b/*','setPrototypeOf','target','*://*.xhamsterlive.com/*','mark','*://*.cam2it.com/b/*','Private\x20element\x20is\x20not\x20present\x20on\x20this\x20object','key','__await','this\x20hasn\x27t\x20been\x20initialised\x20-\x20super()\x20hasn\x27t\x20been\x20called','*://*.homeoffice.live/b/*','LVS_SITE_KEY','*://*.camvirt.com/b/*','*://*.chatur.cc/b/*','return','*://*.chatbang.net/b/*','/.*','resolve','*://*.bongamodels.com/console*','\x27\x20method','replace','Generator\x20is\x20already\x20running','completion','pop','some','object','finallyLoc','XMLHttpRequest','json','origin','*://*.camvoltmodels.com/console*','apply','construct','href','*://*/*','LVS_WORKER_MESSAGE','testbed','LVS_XHR_MESSAGE','*://*.pastabate.com/b/*','catchLoc','[object\x20Generator]','28SBjLgC','toRegExp','complete','*://*.freecams.me/b/*','*://*.chaturbate.su/b/*','tryEntries','name','rval','call','getItem','responseURL','chaturbate','continue','Cannot\x20call\x20a\x20class\x20as\x20a\x20function','async','has','startsWith','\x5c$&','*://*.cht.xxx/*','exports','*://*.chaturbate.asia/b/*','*://*.cam4.com/broadcast*','status','<all_urls>','*://cams.gaypage.com/b/*','bind','asyncIterator','LVS_SERVICE_WORKER_MESSAGE','17930hEQAaQ','writable','*://*.bongamodels2.com/console*','send','WebSocket','message','890QltVjQ','value','*://*.stripchat.com/*','265112CvhUDN','detail','suspendedYield','normal','Generator','illegal\x20catch\x20attempt','stringify','root','string','arg','readystatechange','iterator','prev','*://*.stripchat.global/*','*://*.cumchater.com/*','regeneratorRuntime','url','*://*.chatubate.me/b/*','wrap','SEND_LVS_MESSAGE_BY_WORKER','*://*.chaturbatecams.com/b/*','valueOf','*://*.cams.naughtyads.com.au/b/*','file','afterLoc','readyState','^<all_urls>$|^(?:(?:file://|(?:(\x5c*|<scheme>)://(<host>)))(<path>))$','push','length','stop','clone','dispatchException','charAt','<path>','nextLoc','129771bmOhAM','*://*.mywebcamroom.com/*','*://stripchat.com/cams/*','throw','statusText','*://*.thecarnal.cafe/b/*','symbol','Cannot\x20initialize\x20the\x20same\x20private\x20elements\x20twice\x20on\x20an\x20object','completed','sent','values','*://*.chatyourbate.com/b/*','abrupt','response','arraybuffer','bongacams','(<label>\x5c.)*','*://*.chaterbate.cc/b/*','*://*.nudes.show/b/*','*://*.camsoda.com/*','dispatchEvent','fetch','*://*.superchat.live/*','close','*://*.bongamodels.com/chat-console*','displayName','toStringTag','Worker','2490425IenWYW','hasOwnProperty','test','text','constructor','then','*://*.my.freecams6.com/b/*','executing','*://*.cam4.de.com/broadcast*','end','[0-9A-Za-z]+(?:\x5c-+[0-9A-Za-z]+)*','*://*.xham.live/*','method','awrap','*://*.elitemodelsnow.net/*','(file://|(<scheme>)://<host>)','serviceWorker','*://chat.chaturbate.lu/b/*','1YUNgjB','1572594DZVfYA','type','break','*://*.livelesbiansexcams.com/*','tryLoc','protocol','1830048JGUwYk','GET_LVS_SITE_KEY','*://*.privatecams.com/b/*','GeneratorFunction','@@iterator','keys','LVS_WEB_SOCKET_MESSAGE','responseType','Super\x20expression\x20must\x20either\x20be\x20null\x20or\x20a\x20function','*://*.chaturbate.global/b/*','__proto__','*://*.facetimegirls.com/b/*','_sent','defineProperty','delegate','://<host>','*://*.camgirllover.com/b/*','slice','<path>$'];a7a=function(){return bY;};return a7a();}
\ No newline at end of file
const a8h=a8b;(function(a,b){const g=a8b,c=a();while(!![]){try{const d=-parseInt(g(0x125))/0x1*(parseInt(g(0x123))/0x2)+-parseInt(g(0x12a))/0x3*(-parseInt(g(0x120))/0x4)+-parseInt(g(0x121))/0x5+parseInt(g(0x126))/0x6*(parseInt(g(0x11d))/0x7)+-parseInt(g(0x11f))/0x8+parseInt(g(0x127))/0x9+parseInt(g(0x11b))/0xa;if(d===b)break;else c['push'](c['shift']());}catch(f){c['push'](c['shift']());}}}(a8a,0x8a6d6));let headElem=document[a8h(0x124)](a8h(0x11c));function createScriptElement(a){const i=a8h,b=document['createElement'](i(0x11e));return b['type']=i(0x12b),b['src']=a,b;}function a8b(a,b){const c=a8a();return a8b=function(d,e){d=d-0x119;let f=c[d];return f;},a8b(a,b);}function a8a(){const j=['3068308PjmJiW','1528480UBcMDk','documentElement','50304rKbpGS','createElement','39gGGhbQ','882ahqxjn','2137041sYidJz','remove','runtime','3nEFAfQ','text/javascript','/js/intercept.js','appendChild','9156510OAUHdr','head','45626ajejhQ','script','8197608fPLyEN'];a8a=function(){return j;};return a8a();}try{headElem[a8h(0x11a)](createScriptElement(chrome[a8h(0x129)]['getURL'](a8h(0x119))));}catch(a8c){}document[a8h(0x122)][a8h(0x11a)](headElem),headElem[a8h(0x128)]();
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "Lovense Cam Extension",
"version": "31.5.6",
"description": "Have fun with your Lovense toys by customizing your own levels. ***Note: Remember to enable 'Allow user scripts' after installation. Click on 'Details' and turn on 'Allow user scripts'",
"author": "lovense",
"manifest_version": 3,
"minimum_chrome_version": "120",
"permissions": [
"offscreen",
"userScripts",
"storage",
"unlimitedStorage",
"desktopCapture"
],
"incognito": "split",
"icons": {
"16": "icons/icon-16.png",
"128": "icons/icon-128.png"
},
"options_page": "",
"action": {
"default_title": "Lovense Cam Extension",
"default_popup": "pages/chrome_popup.html"
},
"background": {
"service_worker": "js/service_worker.js"
},
"host_permissions": [
"<all_urls>"
],
"content_scripts": [
{
"js": [
"js/run-at-document-start.js"
],
"matches": [
"<all_urls>"
],
"exclude_matches": [
"*://*.lovense.com/cam-model/*",
"*://*.lovense.com/cam-model-v3/*",
"*://localhost/*"
],
"run_at": "document_start"
}
],
"content_security_policy": {
"extension_pages": "script-src 'self' http://localhost:*; object-src 'self';"
},
"externally_connectable": {
"matches": [
"*://*.lovense.com/cam-model/*",
"*://*.lovense.com/cam-model-v3/*",
"*://localhost/*"
]
},
"web_accessible_resources": [
{
"resources": [
"icons/*",
"img/*",
"js/*"
],
"matches": [
"<all_urls>"
]
}
],
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7zM8HealrzUCVVeJYzx5Vvz3VYwdWyfoQ5Vb7hP34PJvyJ9/5LnvzLoGHaUCjwSLHtT+IqFNkA6OUVhotpZzb4N1zaTq81QP0QnoOerHHQTqkiJyvtaVGiPhZlQ5dDhHoSLZJ1bNaltveGNG3CE2/yicohnJTlXmsh053trrQYVaR6HPJ5lICT37D7jsuMHayFjEsWTlZPLP6Wn80zrLEvpH93oA12UnWyQuPs5z/X5HhyrxzCmeJ4nZXtKuB6ICWU+dHSwzWm4N3l9qCxdvAjgsYwG64+34/i00E8eWgwISeOOVHV2p80UnOanQk7TBfiXJSBQgbosUbLZjmv7y6wIDAQAB"
}
\ No newline at end of file
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Extension Background
</title>
<link rel="icon" href="/favicon.ico">
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
</style>
</head>
<body>
<iframe id="backgroundIframe" width="100%" height="100%" frameborder="0" scrolling="no" allow="bluetooth;hid;serial"></iframe>
<script src="/js/chrome_background.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Extension Popup
</title>
<link rel="icon" href="/favicon.ico">
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
width: 400px;
height: 600px;
}
.popup-container {
width: 100%;
height: 100%;
}
.cam-page-loading {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.loading-block {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
}
.loading-ball {
width: 20px;
height: 20px;
border-radius: 50%;
}
.loading-ball+.loading-ball {
margin-left: 20px;
}
.loading-ball:nth-child(1) {
background-color: #0a0258;
animation: leftBallLoad 1s infinite linear;
}
.loading-ball:nth-child(2) {
background-color: #ff2d89;
animation: rightBallLoad 1s infinite linear;
}
@keyframes leftBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(20px, 0, 0) scale(1.3);
}
50% {
transform: translate3d(40px, 0, 0) scale(1);
}
75% {
transform: translate3d(20px, 0, 0) scale(0.7);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@keyframes rightBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(-20px, 0, 0) scale(0.7);
}
50% {
transform: translate3d(-40px, 0, 0) scale(1);
}
75% {
transform: translate3d(-20px, 0, 0) scale(1.3);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
</style>
</head>
<body>
<div class="popup-container">
<div class="cam-page-loading" id="camPageLoading">
<div class="loading-block">
<div class="loading-ball"></div>
<div class="loading-ball"></div>
</div>
</div>
</div>
<script src="/js/chrome_popup.js"></script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>
Stream Master
</title>
<link rel="icon" href="/cam-model/favicon.ico">
<style>
body {
margin: 0;
overflow: hidden;
}
.cam-page-loading {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
.hidden-cam-page-loading {
display: none !important;
}
.loading-block {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
justify-content: center;
align-items: center;
}
.loading-ball {
width: 20px;
height: 20px;
border-radius: 50%;
}
.loading-ball+.loading-ball {
margin-left: 20px;
}
.loading-ball:nth-child(1) {
background-color: #0a0258;
animation: leftBallLoad 1s infinite linear;
}
.loading-ball:nth-child(2) {
background-color: #ff2d89;
animation: rightBallLoad 1s infinite linear;
}
@keyframes leftBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(20px, 0, 0) scale(1.3);
}
50% {
transform: translate3d(40px, 0, 0) scale(1);
}
75% {
transform: translate3d(20px, 0, 0) scale(0.7);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@keyframes rightBallLoad {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
25% {
transform: translate3d(-20px, 0, 0) scale(0.7);
}
50% {
transform: translate3d(-40px, 0, 0) scale(1);
}
75% {
transform: translate3d(-20px, 0, 0) scale(1.3);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
</style>
</head>
<body>
<div id="app"></div>
<div class="cam-page-loading" id="camPageLoading">
<div class="loading-block">
<div class="loading-ball"></div>
<div class="loading-ball"></div>
</div>
</div>
<script src="/js/stream_master.js"></script>
</body>
</html>
...@@ -18,16 +18,19 @@ import json ...@@ -18,16 +18,19 @@ import json
import shutil import shutil
import asyncio import asyncio
import platform import platform
import signal
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Union, Callable, Any from typing import Dict, List, Optional, Union, Callable, Any
import configparser
from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer, QMimeData, QPoint, QByteArray
from PyQt6.QtGui import QIcon, QAction, QKeySequence from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter, QDrag, QPixmap
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit, QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit,
QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog, QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog,
QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu, QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu,
QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox, QStatusBar, QTabBar, QStyleOptionTab, QFileDialog,
QInputDialog
) )
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings, QWebEnginePage from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings, QWebEnginePage
...@@ -44,15 +47,16 @@ class BrowserTab(QWidget): ...@@ -44,15 +47,16 @@ class BrowserTab(QWidget):
""" """
urlChanged = pyqtSignal(str) urlChanged = pyqtSignal(str)
titleChanged = pyqtSignal(str) titleChanged = pyqtSignal(str)
loadStatusChanged = pyqtSignal()
def __init__(self, parent=None, url="about:blank", user_data_dir=None): def __init__(self, parent=None, url="about:blank", profile=None):
super().__init__(parent) super().__init__(parent)
self.parent = parent self.parent = parent
self.url = url self.url = url
self.user_data_dir = user_data_dir or tempfile.mkdtemp(prefix="qbrowser_") self.profile = profile
self.process = None self.process = None
self.page = None # Playwright page object self.page = None # Playwright page object
self.is_loading = False
# Create a container for the web view # Create a container for the web view
self.layout = QVBoxLayout(self) self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
...@@ -74,11 +78,11 @@ class BrowserTab(QWidget): ...@@ -74,11 +78,11 @@ class BrowserTab(QWidget):
self.web_view.setEnabled(True) # Enable interaction with this view self.web_view.setEnabled(True) # Enable interaction with this view
# Set up a custom profile for this tab # Set up a custom profile for this tab
profile = QWebEngineProfile(self.user_data_dir, self.web_view) page = QWebEnginePage(self.profile, self.web_view)
page = QWebEnginePage(profile, self.web_view)
self.web_view.setPage(page) self.web_view.setPage(page)
# Connect signals # Connect signals
self.web_view.loadStarted.connect(self.on_load_started)
self.web_view.loadFinished.connect(self.on_load_finished) self.web_view.loadFinished.connect(self.on_load_finished)
self.web_view.urlChanged.connect(lambda url: self.urlChanged.emit(url.toString())) self.web_view.urlChanged.connect(lambda url: self.urlChanged.emit(url.toString()))
self.web_view.titleChanged.connect(lambda title: self.titleChanged.emit(title)) self.web_view.titleChanged.connect(lambda title: self.titleChanged.emit(title))
...@@ -104,8 +108,14 @@ class BrowserTab(QWidget): ...@@ -104,8 +108,14 @@ class BrowserTab(QWidget):
if self.page: if self.page:
asyncio.create_task(self.page.goto(url)) asyncio.create_task(self.page.goto(url))
def on_load_started(self):
self.is_loading = True
self.loadStatusChanged.emit()
def on_load_finished(self, success): def on_load_finished(self, success):
"""Handle the page load finishing.""" """Handle the page load finishing."""
self.is_loading = False
self.loadStatusChanged.emit()
if success: if success:
print(f"Page loaded successfully: {self.web_view.url().toString()}") print(f"Page loaded successfully: {self.web_view.url().toString()}")
# Update the title if available # Update the title if available
...@@ -119,13 +129,6 @@ class BrowserTab(QWidget): ...@@ -119,13 +129,6 @@ class BrowserTab(QWidget):
def close(self): def close(self):
"""Close the tab.""" """Close the tab."""
# Clean up the user data directory
try:
if os.path.exists(self.user_data_dir) and self.user_data_dir.startswith(tempfile.gettempdir()):
shutil.rmtree(self.user_data_dir)
except Exception as e:
print(f"Error cleaning up user data directory: {e}")
# Close the web view # Close the web view
if hasattr(self, 'web_view'): if hasattr(self, 'web_view'):
self.web_view.close() self.web_view.close()
...@@ -143,6 +146,7 @@ class ExtensionDialog(QDialog): ...@@ -143,6 +146,7 @@ class ExtensionDialog(QDialog):
self.setMinimumSize(500, 400) self.setMinimumSize(500, 400)
self.extensions_dir = extensions_dir self.extensions_dir = extensions_dir
self.open_popups = []
# Create layout # Create layout
layout = QVBoxLayout(self) layout = QVBoxLayout(self)
...@@ -153,14 +157,17 @@ class ExtensionDialog(QDialog): ...@@ -153,14 +157,17 @@ class ExtensionDialog(QDialog):
# Create buttons # Create buttons
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
self.install_button = QPushButton("Install New Extension") self.install_button = QPushButton("Install New")
self.remove_button = QPushButton("Remove Extension") self.remove_button = QPushButton("Remove")
self.enable_button = QPushButton("Enable/Disable") self.enable_button = QPushButton("Enable/Disable")
self.popup_button = QPushButton("Open Popup")
self.close_button = QPushButton("Close") self.close_button = QPushButton("Close")
button_layout.addWidget(self.install_button) button_layout.addWidget(self.install_button)
button_layout.addWidget(self.remove_button) button_layout.addWidget(self.remove_button)
button_layout.addWidget(self.enable_button) button_layout.addWidget(self.enable_button)
button_layout.addWidget(self.popup_button)
button_layout.addStretch()
button_layout.addWidget(self.close_button) button_layout.addWidget(self.close_button)
layout.addLayout(button_layout) layout.addLayout(button_layout)
...@@ -170,6 +177,7 @@ class ExtensionDialog(QDialog): ...@@ -170,6 +177,7 @@ class ExtensionDialog(QDialog):
self.install_button.clicked.connect(self.install_extension) self.install_button.clicked.connect(self.install_extension)
self.remove_button.clicked.connect(self.remove_extension) self.remove_button.clicked.connect(self.remove_extension)
self.enable_button.clicked.connect(self.toggle_extension) self.enable_button.clicked.connect(self.toggle_extension)
self.popup_button.clicked.connect(self.open_extension_popup)
# Load extensions # Load extensions
self.load_extensions() self.load_extensions()
...@@ -183,40 +191,53 @@ class ExtensionDialog(QDialog): ...@@ -183,40 +191,53 @@ class ExtensionDialog(QDialog):
return return
# Find all extension directories # Find all extension directories
for ext_dir in os.listdir(self.extensions_dir): for ext_name in os.listdir(self.extensions_dir):
ext_path = os.path.join(self.extensions_dir, ext_dir) ext_path = os.path.join(self.extensions_dir, ext_name)
if os.path.isdir(ext_path): if os.path.isdir(ext_path):
# Try to read the manifest enabled = not ext_name.endswith(".disabled")
base_name = ext_name[:-9] if not enabled else ext_name
manifest_path = os.path.join(ext_path, "manifest.json") manifest_path = os.path.join(ext_path, "manifest.json")
if os.path.exists(manifest_path): if os.path.exists(manifest_path):
try: try:
with open(manifest_path, 'r') as f: with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f) manifest = json.load(f)
name = manifest.get("name", ext_dir) name = manifest.get("name", base_name)
version = manifest.get("version", "unknown") version = manifest.get("version", "unknown")
status = "ACTIVE" if enabled else "INACTIVE"
item = QListWidgetItem(f"{name} (v{version})") item = QListWidgetItem(f"{name} (v{version}) - {status}")
item.setData(Qt.ItemDataRole.UserRole, ext_path) item.setData(Qt.ItemDataRole.UserRole, ext_path)
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked)
# Check if extension is enabled
enabled = True # TODO: Implement proper extension state checking
item.setCheckState(
Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked
)
self.extensions_list.addItem(item) self.extensions_list.addItem(item)
except Exception as e: except Exception as e:
print(f"Error loading extension manifest: {e}") print(f"Error loading extension manifest for {ext_name}: {e}")
item = QListWidgetItem(f"{ext_dir} (Error loading manifest)") item = QListWidgetItem(f"{ext_name} (Error loading manifest)")
item.setData(Qt.ItemDataRole.UserRole, ext_path) item.setData(Qt.ItemDataRole.UserRole, ext_path)
self.extensions_list.addItem(item) self.extensions_list.addItem(item)
def install_extension(self): def install_extension(self):
"""Install a new extension.""" """Install a new extension from a directory."""
# TODO: Implement extension installation source_dir = QFileDialog.getExistingDirectory(self, "Select Extension Directory")
print("Extension installation not implemented yet") if not source_dir:
QMessageBox.information(self, "Not Implemented", "Extension installation is not implemented yet.") return
ext_name = os.path.basename(source_dir)
dest_dir = os.path.join(self.extensions_dir, ext_name)
if os.path.exists(dest_dir):
QMessageBox.warning(self, "Extension Exists", f"An extension named '{ext_name}' already exists.")
return
try:
shutil.copytree(source_dir, dest_dir)
self.load_extensions()
QMessageBox.information(self, "Restart Required",
"The extension has been installed. Please restart the browser for it to be loaded.")
except Exception as e:
QMessageBox.critical(self, "Installation Error", f"Could not install the extension: {e}")
def remove_extension(self): def remove_extension(self):
"""Remove the selected extension.""" """Remove the selected extension."""
...@@ -236,91 +257,569 @@ class ExtensionDialog(QDialog): ...@@ -236,91 +257,569 @@ class ExtensionDialog(QDialog):
self.load_extensions() self.load_extensions()
def toggle_extension(self): def toggle_extension(self):
"""Toggle the enabled state of the selected extension.""" """Toggle the enabled state of the selected extension and reload the profile."""
selected_items = self.extensions_list.selectedItems() selected_items = self.extensions_list.selectedItems()
if not selected_items: if not selected_items:
return return
for item in selected_items: item = selected_items[0]
current_state = item.checkState() ext_path = item.data(Qt.ItemDataRole.UserRole)
new_state = Qt.CheckState.Unchecked if current_state == Qt.CheckState.Checked else Qt.CheckState.Checked ext_name = os.path.basename(ext_path)
item.setCheckState(new_state) is_enabled = not ext_name.endswith(".disabled")
if is_enabled:
new_path = ext_path + ".disabled"
else:
new_path = ext_path[:-9]
# TODO: Implement proper extension state saving try:
ext_path = item.data(Qt.ItemDataRole.UserRole) # Rename the folder to enable/disable the extension
enabled = new_state == Qt.CheckState.Checked os.rename(ext_path, new_path)
print(f"Extension {ext_path} {'enabled' if enabled else 'disabled'}")
# Update the list in the dialog
self.load_extensions()
# Get the main browser window and trigger a profile reload
browser = self.parent()
if browser and hasattr(browser, 'reload_profile_and_tabs'):
browser.reload_profile_and_tabs()
else:
# This is a fallback in case the parent isn't the browser window
QMessageBox.information(self, "Restart Required",
"Change applied. Please restart the browser for it to take effect.")
except OSError as e:
QMessageBox.critical(self, "Error", f"Could not change extension state: {e}")
def open_extension_popup(self):
"""Open the popup of the selected extension, if it has one."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
# This is the actual path on disk, e.g., /path/to/ext or /path/to/ext.disabled
ext_path = item.data(Qt.ItemDataRole.UserRole)
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
QMessageBox.warning(self, "Error", "manifest.json not found for this extension.")
return
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not read manifest.json: {e}")
return
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if not popup_path:
QMessageBox.information(self, "No Popup", "This extension does not have a popup.")
return
popup_file = os.path.join(ext_path, popup_path)
if not os.path.exists(popup_file):
QMessageBox.warning(self, "Error", f"Popup file not found: {popup_path}")
return
# Open the popup in a new dialog
popup_dialog = QDialog(self)
popup_dialog.setWindowTitle(manifest.get("name", "Extension Popup"))
layout = QVBoxLayout(popup_dialog)
web_view = QWebEngineView()
web_view.load(QUrl.fromLocalFile(os.path.abspath(popup_file)))
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
self.open_popups.append(popup_dialog)
popup_dialog.finished.connect(lambda: self.open_popups.remove(popup_dialog))
class DetachableTabBar(QTabBar):
tabDetached = pyqtSignal(int, QPoint)
def __init__(self, parent=None):
super().__init__(parent)
self.setAcceptDrops(True)
self.drag_start_pos = QPoint()
self.drag_tab_index = -1
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.drag_start_pos = event.pos()
self.drag_tab_index = self.tabAt(self.drag_start_pos)
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.MouseButton.LeftButton):
return super().mouseMoveEvent(event)
if self.drag_tab_index == -1:
return super().mouseMoveEvent(event)
tab_widget = self.parent().widget(self.drag_tab_index)
if tab_widget and hasattr(tab_widget, 'is_loading') and tab_widget.is_loading:
return # Don't allow dragging while the tab is loading
if (event.pos() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance():
return super().mouseMoveEvent(event)
drag = QDrag(self)
mime_data = QMimeData()
mime_data.setData("application/x-qbrowser-tab-index", QByteArray(str(self.drag_tab_index).encode()))
drag.setMimeData(mime_data)
# Use a simple dummy pixmap to avoid all painter errors
pixmap = QPixmap(100, 30)
pixmap.fill(QColor(53, 53, 53))
painter = QPainter(pixmap)
painter.setPen(Qt.GlobalColor.white)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, self.tabText(self.drag_tab_index))
painter.end()
drag.setPixmap(pixmap)
tab_rect = self.tabRect(self.drag_tab_index)
if tab_rect.isValid():
drag.setHotSpot(event.pos() - tab_rect.topLeft())
else:
drag.setHotSpot(event.pos()) # Fallback hotspot
drop_action = drag.exec(Qt.DropAction.MoveAction)
if drop_action == Qt.DropAction.IgnoreAction:
self.tabDetached.emit(self.drag_tab_index, event.globalPosition().toPoint())
self.drag_tab_index = -1
super().mouseMoveEvent(event)
def dragEnterEvent(self, event):
if event.mimeData().hasFormat("application/x-qbrowser-tab-index"):
event.acceptProposedAction()
else:
super().dragEnterEvent(event)
def dropEvent(self, event):
if not event.mimeData().hasFormat("application/x-qbrowser-tab-index"):
return super().dropEvent(event)
source_tab_bar = event.source()
if not isinstance(source_tab_bar, DetachableTabBar):
return super().dropEvent(event)
source_widget = source_tab_bar.parent()
dest_widget = self.parent()
from_index = int(event.mimeData().data("application/x-qbrowser-tab-index"))
to_index = self.tabAt(event.position().toPoint())
if source_widget == dest_widget:
if to_index == -1:
to_index = self.count()
self.moveTab(from_index, to_index)
self.parent().setCurrentIndex(to_index)
event.acceptProposedAction()
return
# Cross-widget drop
tab_content = source_widget.widget(from_index)
tab_title = source_widget.tabText(from_index)
# Disconnect signals from the old browser
old_browser = tab_content.browser
if old_browser:
try:
tab_content.urlChanged.disconnect(old_browser.update_url)
tab_content.titleChanged.disconnect()
tab_content.loadStatusChanged.disconnect(old_browser.update_status_bar)
except (TypeError, RuntimeError):
pass
# Remove from source widget. The page widget is not deleted.
source_widget.removeTab(from_index)
if to_index == -1:
to_index = self.count()
# Add to destination widget
new_index = dest_widget.insertTab(to_index, tab_content, tab_title)
dest_widget.setCurrentIndex(new_index)
# Connect signals to the new browser
new_browser = dest_widget.window()
tab_content.browser = new_browser
tab_content.urlChanged.connect(new_browser.update_url)
tab_content.titleChanged.connect(lambda title, tab=tab_content: new_browser.update_title(title, tab))
tab_content.loadStatusChanged.connect(new_browser.update_status_bar)
# Close the source window if it has no more tabs
if source_widget.count() == 0:
source_widget.window().close()
event.setDropAction(Qt.DropAction.MoveAction)
event.accept()
class ProfileDialog(QDialog):
"""
Dialog for selecting or creating a browser profile.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Select Profile")
self.setMinimumWidth(400)
self.selected_profile_path = None
self.profiles_dir = "data"
if not os.path.exists(self.profiles_dir):
os.makedirs(self.profiles_dir)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Select a profile to launch:"))
self.profile_list = QListWidget()
self.profile_list.itemDoubleClicked.connect(self.accept_selection)
layout.addWidget(self.profile_list)
button_layout = QHBoxLayout()
self.new_button = QPushButton("Create New")
self.new_button.clicked.connect(self.create_new_profile)
button_layout.addWidget(self.new_button)
self.delete_button = QPushButton("Delete")
self.delete_button.clicked.connect(self.delete_profile)
button_layout.addWidget(self.delete_button)
button_layout.addStretch()
self.select_button = QPushButton("Launch")
self.select_button.setDefault(True)
self.select_button.clicked.connect(self.accept_selection)
button_layout.addWidget(self.select_button)
layout.addLayout(button_layout)
self.load_profiles()
self.update_button_states()
def update_button_states(self):
has_selection = len(self.profile_list.selectedItems()) > 0
self.select_button.setEnabled(has_selection)
self.delete_button.setEnabled(has_selection)
def load_profiles(self):
self.profile_list.clear()
profiles = []
for item in os.listdir(self.profiles_dir):
if os.path.isdir(os.path.join(self.profiles_dir, item)) and item.startswith("browser_profile_"):
profiles.append(item)
for profile_dir_name in sorted(profiles):
profile_name = profile_dir_name.replace("browser_profile_", "")
list_item = QListWidgetItem(profile_name)
list_item.setData(Qt.ItemDataRole.UserRole, os.path.join(self.profiles_dir, profile_dir_name))
self.profile_list.addItem(list_item)
if self.profile_list.count() > 0:
self.profile_list.setCurrentRow(0)
self.profile_list.itemSelectionChanged.connect(self.update_button_states)
self.update_button_states()
def create_new_profile(self):
while True:
profile_name, ok = QInputDialog.getText(self, "Create New Profile", "Enter a name for the new profile:")
if not ok:
return # User cancelled
if not profile_name.strip():
QMessageBox.warning(self, "Invalid Name", "Profile name cannot be empty.")
continue
profile_path = os.path.join(self.profiles_dir, f"browser_profile_{profile_name.strip()}")
if not os.path.exists(profile_path):
try:
os.makedirs(profile_path)
self._install_default_extensions(profile_path)
self.load_profiles()
# Select the new profile
for i in range(self.profile_list.count()):
if self.profile_list.item(i).text() == profile_name.strip():
self.profile_list.setCurrentRow(i)
break
return
except OSError as e:
QMessageBox.critical(self, "Error", f"Could not create profile directory: {e}")
return
else:
QMessageBox.warning(self, "Profile Exists", "A profile with that name already exists.")
def delete_profile(self):
selected_items = self.profile_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
profile_name = item.text()
profile_path = item.data(Qt.ItemDataRole.UserRole)
reply = QMessageBox.question(self, "Confirm Deletion", f"Are you sure you want to delete the profile '{profile_name}'?\nThis will permanently delete all its data.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
try:
shutil.rmtree(profile_path)
self.load_profiles()
self.update_button_states()
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not delete profile: {e}")
def accept_selection(self):
selected_items = self.profile_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
self.selected_profile_path = item.data(Qt.ItemDataRole.UserRole)
self.accept()
@staticmethod
def _install_default_extensions(profile_path):
"""Copies default extensions from assets to a new profile directory."""
source_extensions_dir = "assets/browser/extensions"
if not os.path.isdir(source_extensions_dir):
print(f"Default extensions directory not found: {source_extensions_dir}")
return
dest_extensions_dir = os.path.join(profile_path, "Default", "Extensions")
try:
os.makedirs(dest_extensions_dir, exist_ok=True)
for item_name in os.listdir(source_extensions_dir):
source_item_path = os.path.join(source_extensions_dir, item_name)
dest_item_path = os.path.join(dest_extensions_dir, item_name)
if os.path.isdir(source_item_path):
if not os.path.exists(dest_item_path):
shutil.copytree(source_item_path, dest_item_path)
print(f"Installed default extension '{item_name}' to profile.")
except (OSError, IOError) as e:
QMessageBox.warning(None, "Extension Installation Error",
f"Could not install default extensions: {e}")
@staticmethod
def get_profile_path(parent=None):
profiles_dir = "data"
if not os.path.exists(profiles_dir):
os.makedirs(profiles_dir)
profiles = [p for p in os.listdir(profiles_dir) if p.startswith("browser_profile_")]
if not profiles:
# First run experience: must create a profile to continue.
while True:
profile_name, ok = QInputDialog.getText(parent, "Create First Profile", "Welcome! Please create a profile to begin:")
if not ok:
return None # User cancelled initial creation, so exit.
if not profile_name.strip():
QMessageBox.warning(parent, "Invalid Name", "Profile name cannot be empty.")
continue
profile_path = os.path.join(profiles_dir, f"browser_profile_{profile_name.strip()}")
if os.path.exists(profile_path):
QMessageBox.warning(parent, "Profile Exists", "A profile with that name already exists. Please choose another name.")
continue
try:
os.makedirs(profile_path)
ProfileDialog._install_default_extensions(profile_path)
return profile_path # Success!
except OSError as e:
QMessageBox.critical(parent, "Error", f"Could not create profile directory: {e}")
return None
else:
# Profiles exist, show the manager dialog.
dialog = ProfileDialog(parent)
if dialog.exec() == QDialog.DialogCode.Accepted:
return dialog.selected_profile_path
else:
return None # User closed the manager.
class Browser(QMainWindow): class Browser(QMainWindow):
""" """
Main browser window that wraps the Playwright browser class. Main browser window that wraps the Playwright browser class.
""" """
def __init__(self): instances = []
def __init__(self, initial_url=None, debug=False, detached_tab=None, profile_path=None):
super().__init__() super().__init__()
self.setWindowTitle("QBrowser") Browser.instances.append(self)
self.setWindowTitle("SHMCamStudio Browser")
self.setWindowIcon(QIcon('assets/logo.jpg'))
self.resize(1024, 768) self.resize(1024, 768)
# Create a central widget and layout # Create a central widget and layout
self.central_widget = QWidget() self.central_widget = QWidget()
self.setCentralWidget(self.central_widget) self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout(self.central_widget) self.layout = QVBoxLayout(self.central_widget)
self.layout.setContentsMargins(0, 0, 0, 0) self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0) self.layout.setSpacing(0)
# Create toolbar # Create toolbar
self.toolbar = QToolBar() self.toolbar = QToolBar()
self.toolbar.setMovable(False) self.toolbar.setMovable(False)
self.toolbar.setIconSize(QSize(16, 16)) self.toolbar.setIconSize(QSize(24, 24))
# Add back button
self.back_button = QPushButton()
self.back_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowBack))
self.back_button.setToolTip("Back")
self.back_button.clicked.connect(self.navigate_back)
self.toolbar.addWidget(self.back_button)
# Add forward button
self.forward_button = QPushButton()
self.forward_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowForward))
self.forward_button.setToolTip("Forward")
self.forward_button.clicked.connect(self.navigate_forward)
self.toolbar.addWidget(self.forward_button)
# Add reload button
self.reload_button = QPushButton()
self.reload_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_BrowserReload))
self.reload_button.setToolTip("Reload (F5)")
self.reload_button.clicked.connect(self.reload_page)
self.toolbar.addWidget(self.reload_button)
# Add home button # Add home button
self.home_button = QPushButton() self.home_button = QPushButton()
self.home_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowBack)) self.home_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_DirHomeIcon))
self.home_button.setToolTip("Home") self.home_button.setToolTip("Home")
self.home_button.clicked.connect(self.navigate_home) self.home_button.clicked.connect(self.navigate_home)
self.toolbar.addWidget(self.home_button) self.toolbar.addWidget(self.home_button)
# Add URL input # Add URL input
self.url_input = QLineEdit() self.url_input = QLineEdit()
self.url_input.setPlaceholderText("Enter URL...") self.url_input.setPlaceholderText("Enter URL...")
self.url_input.returnPressed.connect(self.navigate_to_url) self.url_input.returnPressed.connect(self.navigate_to_url)
self.toolbar.addWidget(self.url_input) self.toolbar.addWidget(self.url_input)
# Add extensions button # Add extensions button
self.extensions_button = QPushButton() self.extensions_button = QPushButton()
self.extensions_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_FileDialogDetailedView)) self.extensions_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
self.extensions_button.setToolTip("Extensions") self.extensions_button.setToolTip("Extensions")
self.extensions_button.clicked.connect(self.show_extensions) self.extensions_button.clicked.connect(self.show_extensions)
self.toolbar.addWidget(self.extensions_button) self.toolbar.addWidget(self.extensions_button)
# Add new window button
self.new_window_button = QPushButton()
self.new_window_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileIcon))
self.new_window_button.setToolTip("New Window (Ctrl+W)")
self.new_window_button.clicked.connect(self.open_new_window)
self.toolbar.addWidget(self.new_window_button)
if debug:
self.debug_button = QPushButton()
self.debug_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_MessageBoxWarning))
self.debug_button.setToolTip("Toggle Developer Tools")
self.debug_button.clicked.connect(self.toggle_devtools)
self.toolbar.addWidget(self.debug_button)
self.layout.addWidget(self.toolbar) self.layout.addWidget(self.toolbar)
# Create tab widget # Create tab widget
self.tabs = QTabWidget() self.tabs = QTabWidget()
self.tab_bar = DetachableTabBar()
self.tabs.setTabBar(self.tab_bar)
self.tab_bar.tabDetached.connect(self.handle_tab_detached)
self.tabs.setTabsClosable(True) self.tabs.setTabsClosable(True)
self.tabs.setMovable(True) # self.tabs.setMovable(True) # This is handled by our DetachableTabBar
self.tabs.setDocumentMode(True) self.tabs.setDocumentMode(True)
self.tabs.tabCloseRequested.connect(self.close_tab) self.tabs.tabCloseRequested.connect(self.close_tab)
self.tabs.currentChanged.connect(self.tab_changed) self.tabs.currentChanged.connect(self.tab_changed)
self.layout.addWidget(self.tabs) self.layout.addWidget(self.tabs)
# Add status bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# Add a new tab button # Add a new tab button
self.tabs.setCornerWidget(self._create_new_tab_button(), Qt.Corner.TopRightCorner) self.tabs.setCornerWidget(self._create_new_tab_button(), Qt.Corner.TopRightCorner)
# Set the home URL # Set the home URL from config
self.home_url = "https://www.google.com" config = configparser.ConfigParser()
config.read('shmcamstudio.conf')
# Create a new tab on startup self.home_url = config.get('Browser', 'home_url', fallback='https://www.google.com')
self.new_page(self.home_url)
# Centralized browser profile
if not profile_path:
# This should not happen if main is used, but as a fallback
profile_path = "data/browser_profile_default"
self.profile_dir = profile_path
if not os.path.exists(self.profile_dir):
os.makedirs(self.profile_dir)
# Extensions must be in a specific subdirectory of the profile
self.extensions_dir = os.path.join(self.profile_dir, "Default", "Extensions")
if not os.path.exists(self.extensions_dir):
os.makedirs(self.extensions_dir)
self.profile = QWebEngineProfile(self.profile_dir, self)
if detached_tab:
widget, title = detached_tab
index = self.tabs.addTab(widget, title)
self.tabs.setCurrentIndex(index)
widget.browser = self
widget.urlChanged.connect(self.update_url)
widget.titleChanged.connect(lambda t, tab=widget: self.update_title(t, tab))
widget.loadStatusChanged.connect(self.update_status_bar)
if hasattr(widget, 'web_view'):
self.update_title(widget.web_view.title(), widget)
self.update_url(widget.web_view.url().toString())
else:
# Create a new tab on startup
url_to_open = initial_url if initial_url is not None else self.home_url
self.new_page(url_to_open)
# Set up keyboard shortcuts # Set up keyboard shortcuts
self._setup_shortcuts() self._setup_shortcuts()
# Store Playwright objects # Store Playwright objects
self.playwright = None self.playwright = None
self.playwright_browser = None self.playwright_browser = None
self.browser_contexts = {} self.browser_contexts = {}
self.dev_tools_windows = []
def closeEvent(self, event):
if self in Browser.instances:
Browser.instances.remove(self)
super().closeEvent(event)
def _get_themed_icon(self, standard_pixmap, color="white"):
"""Create a themed icon from a standard pixmap."""
icon = self.style().standardIcon(standard_pixmap)
pixmap = icon.pixmap(self.toolbar.iconSize())
painter = QPainter(pixmap)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
painter.fillRect(pixmap.rect(), QColor(color))
painter.end()
return QIcon(pixmap)
def _create_new_tab_button(self): def _create_new_tab_button(self):
"""Create a button for adding new tabs.""" """Create a button for adding new tabs."""
...@@ -336,27 +835,82 @@ class Browser(QMainWindow): ...@@ -336,27 +835,82 @@ class Browser(QMainWindow):
new_tab_shortcut.setShortcut(QKeySequence("Ctrl+T")) new_tab_shortcut.setShortcut(QKeySequence("Ctrl+T"))
new_tab_shortcut.triggered.connect(lambda: self.new_page(self.home_url)) new_tab_shortcut.triggered.connect(lambda: self.new_page(self.home_url))
self.addAction(new_tab_shortcut) self.addAction(new_tab_shortcut)
# Ctrl+W: Close Tab # Ctrl+Tab: Cycle Tabs
close_tab_shortcut = QAction("Close Tab", self) cycle_tabs_shortcut = QAction("Cycle Tabs", self)
close_tab_shortcut.setShortcut(QKeySequence("Ctrl+W")) cycle_tabs_shortcut.setShortcut(QKeySequence("Ctrl+Tab"))
close_tab_shortcut.triggered.connect(lambda: self.close_tab(self.tabs.currentIndex())) cycle_tabs_shortcut.triggered.connect(self.cycle_tabs)
self.addAction(close_tab_shortcut) self.addAction(cycle_tabs_shortcut)
# Ctrl+W: New Window
new_window_shortcut = QAction("New Window", self)
new_window_shortcut.setShortcut(QKeySequence("Ctrl+W"))
new_window_shortcut.triggered.connect(self.open_new_window)
self.addAction(new_window_shortcut)
# Ctrl+L: Focus URL bar # Ctrl+L: Focus URL bar
focus_url_shortcut = QAction("Focus URL", self) focus_url_shortcut = QAction("Focus URL", self)
focus_url_shortcut.setShortcut(QKeySequence("Ctrl+L")) focus_url_shortcut.setShortcut(QKeySequence("Ctrl+L"))
focus_url_shortcut.triggered.connect(self.url_input.setFocus) focus_url_shortcut.triggered.connect(self.url_input.setFocus)
self.addAction(focus_url_shortcut) self.addAction(focus_url_shortcut)
# F5: Reload page
reload_shortcut = QAction("Reload", self)
reload_shortcut.setShortcut(QKeySequence("F5"))
reload_shortcut.triggered.connect(self.reload_page)
self.addAction(reload_shortcut)
def cycle_tabs(self):
"""Cycle through the open tabs."""
if self.tabs.count() > 1:
next_index = (self.tabs.currentIndex() + 1) % self.tabs.count()
self.tabs.setCurrentIndex(next_index)
def on_tab_removed(self):
"""Close the window if the last tab is removed."""
if self.tabs.count() == 0:
self.close()
def handle_tab_detached(self, index, pos):
"""Handle a tab being detached to a new window."""
if index < 0 or index >= self.tabs.count():
return
widget = self.tabs.widget(index)
title = self.tabs.tabText(index)
try:
widget.urlChanged.disconnect(self.update_url)
widget.titleChanged.disconnect()
widget.loadStatusChanged.disconnect(self.update_status_bar)
except (TypeError, RuntimeError):
pass
# Create a new browser instance, which will take ownership of the widget.
new_browser = Browser(
debug=("--debug" in sys.argv),
detached_tab=(widget, title),
profile_path=self.profile_dir
)
new_browser.setGeometry(self.geometry())
new_browser.move(pos - QPoint(new_browser.width() // 4, 10))
new_browser.show()
# Now that the widget is safely reparented, remove the tab from the old browser.
# The on_tab_removed method will handle closing the window if it's empty.
self.tabs.removeTab(index)
self.on_tab_removed()
def new_page(self, url="about:blank"): def new_page(self, url="about:blank"):
"""Create a new browser tab with the specified URL.""" """Create a new browser tab with the specified URL."""
# Create a new tab # Create a new tab
tab = BrowserTab(parent=self.tabs, url=url) tab = BrowserTab(parent=self.tabs, url=url, profile=self.profile)
# Store a reference to the Browser instance # Store a reference to the Browser instance
tab.browser = self tab.browser = self
tab.urlChanged.connect(self.update_url) tab.urlChanged.connect(self.update_url)
tab.titleChanged.connect(lambda title, tab=tab: self.update_title(title, tab)) tab.titleChanged.connect(lambda title, tab=tab: self.update_title(title, tab))
tab.loadStatusChanged.connect(self.update_status_bar)
# Add the tab to the tab widget # Add the tab to the tab widget
index = self.tabs.addTab(tab, "New Tab") index = self.tabs.addTab(tab, "New Tab")
...@@ -369,19 +923,14 @@ class Browser(QMainWindow): ...@@ -369,19 +923,14 @@ class Browser(QMainWindow):
if index < 0 or index >= self.tabs.count(): if index < 0 or index >= self.tabs.count():
return return
# Get the tab widget # Get the tab widget and schedule it for deletion.
tab = self.tabs.widget(index) tab = self.tabs.widget(index)
# Close the tab
self.tabs.removeTab(index)
# Close the tab widget
if tab: if tab:
tab.close() tab.deleteLater()
# If no tabs are left, create a new one # Remove the tab from the tab bar.
if self.tabs.count() == 0: self.tabs.removeTab(index)
self.new_page(self.home_url) self.on_tab_removed()
def tab_changed(self, index): def tab_changed(self, index):
"""Handle tab change events.""" """Handle tab change events."""
...@@ -392,6 +941,7 @@ class Browser(QMainWindow): ...@@ -392,6 +941,7 @@ class Browser(QMainWindow):
tab = self.tabs.widget(index) tab = self.tabs.widget(index)
if hasattr(tab, 'url'): if hasattr(tab, 'url'):
self.url_input.setText(tab.url) self.url_input.setText(tab.url)
self.update_status_bar()
def update_url(self, url): def update_url(self, url):
"""Update the URL input when the page URL changes.""" """Update the URL input when the page URL changes."""
...@@ -426,28 +976,104 @@ class Browser(QMainWindow): ...@@ -426,28 +976,104 @@ class Browser(QMainWindow):
current_tab = self.tabs.currentWidget() current_tab = self.tabs.currentWidget()
if current_tab: if current_tab:
current_tab.navigate(self.home_url) current_tab.navigate(self.home_url)
def navigate_back(self):
"""Navigate back in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.back()
def navigate_forward(self):
"""Navigate forward in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.forward()
def reload_page(self):
"""Reload the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.reload()
def open_new_window(self):
"""Open a new browser window."""
new_browser = Browser(debug="--debug" in sys.argv, profile_path=self.profile_dir)
new_browser.show()
def toggle_devtools(self):
"""Opens the remote developer tools URL in a new window."""
dev_tools_window = QDialog(self)
dev_tools_window.setWindowTitle("Developer Tools")
dev_tools_window.resize(1200, 800)
layout = QVBoxLayout()
web_view = QWebEngineView()
web_view.load(QUrl("http://localhost:9222"))
layout.addWidget(web_view)
dev_tools_window.setLayout(layout)
dev_tools_window.show()
self.dev_tools_windows.append(dev_tools_window)
dev_tools_window.finished.connect(lambda: self.dev_tools_windows.remove(dev_tools_window))
def update_status_bar(self):
"""Update the status bar based on the current tab's state."""
current_tab = self.tabs.currentWidget()
if not current_tab:
self.status_bar.showMessage("Ready")
return
if current_tab.is_loading:
self.status_bar.showMessage("Loading...")
else:
self.status_bar.showMessage("Ready")
def reload_profile_and_tabs(self):
"""Reloads the browser profile and recreates pages in all tabs."""
print("Reloading profile to apply extension changes...")
# 1. Save the URLs and current index of all open tabs
urls = []
current_index = self.tabs.currentIndex()
for i in range(self.tabs.count()):
tab = self.tabs.widget(i)
if hasattr(tab, 'web_view') and tab.web_view.url().isValid():
urls.append(tab.web_view.url().toString())
else:
urls.append("about:blank")
# 2. Store the old profile to delete it later
old_profile = self.profile
old_profile.clearHttpCache()
# 3. Clear all existing tabs. This removes references to pages using the old profile.
self.tabs.clear()
# 4. Create a new profile object. This forces a re-read from disk because
# the old one will be properly deleted.
self.profile = QWebEngineProfile(self.profile_dir, self)
# 5. Re-create all tabs with the new profile
if not urls: # Ensure there's at least one tab
urls.append(self.home_url)
for url in urls:
self.new_page(url)
# 6. Restore the previously active tab
if current_index != -1 and current_index < len(urls):
self.tabs.setCurrentIndex(current_index)
# 7. Schedule the old profile for deletion. This is crucial.
old_profile.setParent(None)
old_profile.deleteLater()
print("Profile and tabs reloaded.")
def show_extensions(self): def show_extensions(self):
"""Show the extensions dialog.""" """Show the extensions dialog."""
# Find the extensions directory dialog = ExtensionDialog(self, self.extensions_dir)
# This is a simplified approach; in reality, you'd need to find the actual extensions directory
user_data_dir = None
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'user_data_dir'):
user_data_dir = current_tab.user_data_dir
extensions_dir = None
if user_data_dir:
possible_ext_dirs = [
os.path.join(user_data_dir, "Extensions"),
os.path.join(user_data_dir, "Default", "Extensions")
]
for dir_path in possible_ext_dirs:
if os.path.exists(dir_path) and os.path.isdir(dir_path):
extensions_dir = dir_path
break
dialog = ExtensionDialog(self, extensions_dir)
dialog.exec() dialog.exec()
...@@ -652,18 +1278,59 @@ async def main_async(): ...@@ -652,18 +1278,59 @@ async def main_async():
def main(): def main():
"""Main function to run the browser as a standalone application.""" """Main function to run the browser as a standalone application."""
debug_mode = "--debug" in sys.argv
if debug_mode:
os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = "9222"
app = QApplication.instance() or QApplication(sys.argv) app = QApplication.instance() or QApplication(sys.argv)
# Allow Ctrl+C to kill the application gracefully
signal.signal(signal.SIGINT, signal.SIG_DFL)
app.setWindowIcon(QIcon('assets/logo.jpg'))
# Set a modern, dark theme
app.setStyle("Fusion")
dark_palette = app.palette()
dark_palette.setColor(dark_palette.ColorRole.Window, QColor(45, 45, 45))
dark_palette.setColor(dark_palette.ColorRole.WindowText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Base, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ToolTipBase, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.ToolTipText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Text, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Button, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ButtonText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.BrightText, Qt.GlobalColor.red)
dark_palette.setColor(dark_palette.ColorRole.Link, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.Highlight, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.HighlightedText, Qt.GlobalColor.black)
app.setPalette(dark_palette)
# Set a better font
font = QFont("Cantarell", 10)
if platform.system() == "Windows":
font = QFont("Segoe UI", 10)
elif platform.system() == "Darwin":
font = QFont("San Francisco", 10)
app.setFont(font)
# Create a dummy parent for dialogs to ensure they are properly modal
dummy_parent = QWidget()
# Get profile path
profile_path = ProfileDialog.get_profile_path(dummy_parent)
if not profile_path:
return 0 # Exit gracefully
# Create a simple browser directly without Playwright for now # Create a simple browser directly without Playwright for now
# This ensures we at least get a window showing # This ensures we at least get a window showing
browser = Browser() initial_url = None
if len(sys.argv) > 1 and not sys.argv[1].startswith('--'):
initial_url = sys.argv[1]
browser = Browser(initial_url=initial_url, debug=debug_mode, profile_path=profile_path)
browser.show() browser.show()
# If a URL is provided as a command-line argument, navigate to it
if len(sys.argv) > 1:
url = sys.argv[1]
browser.new_page(url)
print("Browser window should be visible now") print("Browser window should be visible now")
# Run the Qt event loop # Run the Qt event loop
...@@ -671,4 +1338,4 @@ def main(): ...@@ -671,4 +1338,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
sys.exit(main()) sys.exit(main())
\ No newline at end of file
import asyncio #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
qbrowser.py - A Qt6-based browser that wraps and extends the Playwright browser class
This module provides a Qt6-based browser interface that wraps the Playwright browser
API and launches Chromium browser instances without decorations using the --app flag.
Each new page is opened as a new tab in the Qt6 window, and the interface includes
URL input, home button, and browser extension management.
"""
import os
import sys
import subprocess
import tempfile
import json import json
from playwright.async_api import async_playwright, Browser, BrowserContext, Page import shutil
from PyQt6.QtWidgets import QApplication, QMainWindow, QTabWidget, QWidget, QPushButton, QHBoxLayout, QVBoxLayout import asyncio
import platform
import signal
from pathlib import Path
from typing import Dict, List, Optional, Union, Callable, Any
import configparser
from functools import partial
from PyQt6.QtCore import Qt, QUrl, QProcess, pyqtSignal, QSize, QRect, QTimer, QMimeData, QPoint, QByteArray, QBuffer
from PyQt6.QtGui import QIcon, QAction, QKeySequence, QFont, QColor, QPainter, QDrag, QPixmap
from PyQt6.QtWidgets import (
QApplication, QMainWindow, QTabWidget, QToolBar, QLineEdit,
QPushButton, QWidget, QVBoxLayout, QHBoxLayout, QDialog,
QLabel, QListWidget, QListWidgetItem, QCheckBox, QMenu,
QSizePolicy, QStyle, QFrame, QSplitter, QMessageBox, QStatusBar, QTabBar, QStyleOptionTab, QFileDialog,
QInputDialog
)
from PyQt6.QtWebEngineWidgets import QWebEngineView from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtCore import QUrl, QTimer, Qt from PyQt6.QtWebEngineCore import (
import sys QWebEngineProfile, QWebEngineSettings, QWebEnginePage, QWebEngineUrlSchemeHandler,
import threading QWebEngineUrlRequestJob
import time )
from typing import Optional, List import mimetypes
# Import Playwright for API compatibility
from playwright.async_api import async_playwright, Browser as PlaywrightBrowser
from playwright.async_api import Page as PlaywrightPage
from playwright.async_api import BrowserContext as PlaywrightBrowserContext
class BrowserTab(QWidget):
"""
A tab widget that contains a web view and manages a Chromium process.
"""
urlChanged = pyqtSignal(str)
titleChanged = pyqtSignal(str)
loadStatusChanged = pyqtSignal()
def __init__(self, parent=None, url="about:blank", profile=None):
super().__init__(parent)
self.parent = parent
self.url = url
self.profile = profile
self.process = None
self.page = None # Playwright page object
self.is_loading = False
# Create a container for the web view
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Create a placeholder widget until the Chromium process is ready
self.placeholder = QLabel("Loading browser...")
self.placeholder.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.layout.addWidget(self.placeholder)
# Start the Chromium process
self.start_browser()
def start_browser(self):
"""Initialize the QWebEngineView for browsing."""
# Create a QWebEngineView for the Qt UI
# This will be the actual browser, no separate Chromium process
self.web_view = QWebEngineView()
self.web_view.setEnabled(True) # Enable interaction with this view
# Set up a custom profile for this tab
page = QWebEnginePage(self.profile, self.web_view)
self.web_view.setPage(page)
# Connect signals
self.web_view.loadStarted.connect(self.on_load_started)
self.web_view.loadFinished.connect(self.on_load_finished)
self.web_view.urlChanged.connect(lambda url: self.urlChanged.emit(url.toString()))
self.web_view.titleChanged.connect(lambda title: self.titleChanged.emit(title))
# Load the URL
self.web_view.load(QUrl(self.url))
# Replace the placeholder with the web view
self.layout.removeWidget(self.placeholder)
self.placeholder.deleteLater()
self.layout.addWidget(self.web_view)
print(f"Loading URL in QWebEngineView: {self.url}")
def navigate(self, url):
"""Navigate to the specified URL."""
self.url = url
if hasattr(self, 'web_view'):
print(f"Navigating to: {url}")
self.web_view.load(QUrl(url))
# If we have a Playwright page, navigate it as well for API compatibility
if self.page:
asyncio.create_task(self.page.goto(url))
def on_load_started(self):
self.is_loading = True
self.loadStatusChanged.emit()
def on_load_finished(self, success):
"""Handle the page load finishing."""
self.is_loading = False
self.loadStatusChanged.emit()
if success:
print(f"Page loaded successfully: {self.web_view.url().toString()}")
# Update the title if available
title = self.web_view.title()
if title:
self.titleChanged.emit(title)
else:
print(f"Failed to load page: {self.url}")
# We no longer need process handling methods since we're using QWebEngineView directly
def close(self):
"""Close the tab."""
# Close the web view
if hasattr(self, 'web_view'):
self.web_view.close()
super().close()
class ExtensionSchemeHandler(QWebEngineUrlSchemeHandler):
"""A custom URL scheme handler for loading extension files."""
def __init__(self, extensions_dir, parent=None):
super().__init__(parent)
self.extensions_dir = extensions_dir
self.jobs = {} # Keep track of active jobs and their buffers
def requestStarted(self, job: QWebEngineUrlRequestJob):
url = job.requestUrl()
ext_id = url.host()
resource_path = url.path().lstrip('/')
file_path = os.path.abspath(os.path.join(self.extensions_dir, ext_id, resource_path))
try:
with open(file_path, 'rb') as f:
content = f.read()
mime_type, _ = mimetypes.guess_type(file_path)
if mime_type:
mime_type = mime_type.encode()
else:
mime_type = b'application/octet-stream'
buf = QBuffer()
buf.setData(content)
buf.open(QBuffer.OpenModeFlag.ReadOnly)
# Keep the buffer and job object alive until the job is finished.
self.jobs[job] = buf
job.finished.connect(self.on_job_finished)
job.reply(mime_type, buf)
except FileNotFoundError:
print(f"Extension resource not found: {file_path}")
job.fail(QWebEngineUrlRequestJob.Error.UrlNotFound)
except Exception as e:
print(f"Error loading extension resource {file_path}: {e}")
job.fail(QWebEngineUrlRequestJob.Error.RequestFailed)
def on_job_finished(self):
"""Clean up the job and buffer by removing them from the tracking dict."""
job = self.sender()
if job in self.jobs:
del self.jobs[job]
class ExtensionDialog(QDialog):
"""
Dialog for managing browser extensions.
"""
def __init__(self, parent=None, extensions_dir=None):
super().__init__(parent)
self.setWindowTitle("Browser Extensions")
self.setMinimumSize(500, 400)
self.extensions_dir = extensions_dir
self.open_popups = []
# Create layout
layout = QVBoxLayout(self)
# Create extensions list
self.extensions_list = QListWidget()
layout.addWidget(self.extensions_list)
# Create buttons
button_layout = QHBoxLayout()
self.install_button = QPushButton("Install New")
self.remove_button = QPushButton("Remove")
self.enable_button = QPushButton("Enable/Disable")
self.popup_button = QPushButton("Open Popup")
self.close_button = QPushButton("Close")
button_layout.addWidget(self.install_button)
button_layout.addWidget(self.remove_button)
button_layout.addWidget(self.enable_button)
button_layout.addWidget(self.popup_button)
button_layout.addStretch()
button_layout.addWidget(self.close_button)
layout.addLayout(button_layout)
# Connect signals
self.close_button.clicked.connect(self.accept)
self.install_button.clicked.connect(self.install_extension)
self.remove_button.clicked.connect(self.remove_extension)
self.enable_button.clicked.connect(self.toggle_extension)
self.popup_button.clicked.connect(self.open_extension_popup)
# Load extensions
self.load_extensions()
def load_extensions(self):
"""Load and display installed extensions."""
self.extensions_list.clear()
if not self.extensions_dir or not os.path.exists(self.extensions_dir):
self.extensions_list.addItem("No extensions directory found")
return
# Find all extension directories
for ext_name in os.listdir(self.extensions_dir):
ext_path = os.path.join(self.extensions_dir, ext_name)
if os.path.isdir(ext_path):
enabled = not ext_name.endswith(".disabled")
base_name = ext_name[:-9] if not enabled else ext_name
manifest_path = os.path.join(ext_path, "manifest.json")
if os.path.exists(manifest_path):
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
name = manifest.get("name", base_name)
version = manifest.get("version", "unknown")
status = "ACTIVE" if enabled else "INACTIVE"
item = QListWidgetItem(f"{name} (v{version}) - {status}")
item.setData(Qt.ItemDataRole.UserRole, ext_path)
item.setCheckState(Qt.CheckState.Checked if enabled else Qt.CheckState.Unchecked)
self.extensions_list.addItem(item)
except Exception as e:
print(f"Error loading extension manifest for {ext_name}: {e}")
item = QListWidgetItem(f"{ext_name} (Error loading manifest)")
item.setData(Qt.ItemDataRole.UserRole, ext_path)
self.extensions_list.addItem(item)
def install_extension(self):
"""Install a new extension from a directory."""
source_dir = QFileDialog.getExistingDirectory(self, "Select Extension Directory")
if not source_dir:
return
ext_name = os.path.basename(source_dir)
dest_dir = os.path.join(self.extensions_dir, ext_name)
if os.path.exists(dest_dir):
QMessageBox.warning(self, "Extension Exists", f"An extension named '{ext_name}' already exists.")
return
try:
shutil.copytree(source_dir, dest_dir)
self.load_extensions()
QMessageBox.information(self, "Restart Required",
"The extension has been installed. Please restart the browser for it to be loaded.")
except Exception as e:
QMessageBox.critical(self, "Installation Error", f"Could not install the extension: {e}")
def remove_extension(self):
"""Remove the selected extension."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
for item in selected_items:
ext_path = item.data(Qt.ItemDataRole.UserRole)
if ext_path and os.path.exists(ext_path):
try:
shutil.rmtree(ext_path)
print(f"Removed extension: {ext_path}")
except Exception as e:
print(f"Error removing extension: {e}")
self.load_extensions()
def toggle_extension(self):
"""Toggle the enabled state of the selected extension and reload the profile."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
ext_path = item.data(Qt.ItemDataRole.UserRole)
ext_name = os.path.basename(ext_path)
is_enabled = not ext_name.endswith(".disabled")
if is_enabled:
new_path = ext_path + ".disabled"
else:
new_path = ext_path[:-9]
try:
# Rename the folder to enable/disable the extension
os.rename(ext_path, new_path)
# Update the list in the dialog
self.load_extensions()
# Get the main browser window and trigger a profile reload
browser = self.parent()
if browser and hasattr(browser, 'reload_profile_and_tabs'):
browser.reload_profile_and_tabs()
else:
# This is a fallback in case the parent isn't the browser window
QMessageBox.information(self, "Restart Required",
"Change applied. Please restart the browser for it to take effect.")
except OSError as e:
QMessageBox.critical(self, "Error", f"Could not change extension state: {e}")
def open_extension_popup(self):
"""Open the popup of the selected extension, if it has one."""
selected_items = self.extensions_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
ext_path = item.data(Qt.ItemDataRole.UserRole)
ext_name = os.path.basename(ext_path)
if ext_name.endswith(".disabled"):
QMessageBox.warning(self, "Extension Disabled", "Cannot open popup for a disabled extension.")
return
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
QMessageBox.warning(self, "Error", "manifest.json not found for this extension.")
return
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not read manifest.json: {e}")
return
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
if not popup_path:
QMessageBox.information(self, "No Popup", "This extension does not have a popup.")
return
popup_file = os.path.join(ext_path, popup_path)
if not os.path.exists(popup_file):
QMessageBox.warning(self, "Error", f"Popup file not found: {popup_path}")
return
# Open the popup in a new dialog
popup_dialog = QDialog(self)
popup_dialog.setWindowTitle(manifest.get("name", "Extension Popup"))
layout = QVBoxLayout(popup_dialog)
web_view = QWebEngineView()
browser_window = self.parent()
if not browser_window or not isinstance(browser_window, Browser):
QMessageBox.critical(self, "Error", "Could not get browser window reference.")
return
# Use the main browser's profile so the scheme handler is available
page = QWebEnginePage(browser_window.profile, web_view)
web_view.setPage(page)
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
self.open_popups.append(popup_dialog)
popup_dialog.finished.connect(lambda: self.open_popups.remove(popup_dialog))
class DetachableTabBar(QTabBar):
tabDetached = pyqtSignal(int, QPoint)
class CloseableTabWidget(QTabWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setTabsClosable(False) # We'll manage close buttons manually self.setAcceptDrops(True)
self.drag_start_pos = QPoint()
self.drag_tab_index = -1
def addTab(self, widget: QWebEngineView, title: str) -> int: def mousePressEvent(self, event):
# Create a container widget for the tab if event.button() == Qt.MouseButton.LeftButton:
container = QWidget() self.drag_start_pos = event.pos()
layout = QVBoxLayout() self.drag_tab_index = self.tabAt(self.drag_start_pos)
layout.setContentsMargins(0, 0, 0, 0) super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if not (event.buttons() & Qt.MouseButton.LeftButton):
return super().mouseMoveEvent(event)
if self.drag_tab_index == -1:
return super().mouseMoveEvent(event)
# Add web view tab_widget = self.parent().widget(self.drag_tab_index)
layout.addWidget(widget) if tab_widget and hasattr(tab_widget, 'is_loading') and tab_widget.is_loading:
return # Don't allow dragging while the tab is loading
# Create header with title and close button if (event.pos() - self.drag_start_pos).manhattanLength() < QApplication.startDragDistance():
header = QWidget() return super().mouseMoveEvent(event)
header_layout = QHBoxLayout()
header_layout.setContentsMargins(0, 0, 0, 0)
# Close button drag = QDrag(self)
close_button = QPushButton("X") mime_data = QMimeData()
close_button.setFixedSize(20, 20) mime_data.setData("application/x-qbrowser-tab-index", QByteArray(str(self.drag_tab_index).encode()))
close_button.setStyleSheet("font-weight: bold; color: red; border: none;") drag.setMimeData(mime_data)
close_button.clicked.connect(lambda: self._close_tab(self.indexOf(container)))
header_layout.addStretch() # Use a simple dummy pixmap to avoid all painter errors
header_layout.addWidget(close_button) pixmap = QPixmap(100, 30)
header.setLayout(header_layout) pixmap.fill(QColor(53, 53, 53))
painter = QPainter(pixmap)
painter.setPen(Qt.GlobalColor.white)
painter.drawText(pixmap.rect(), Qt.AlignmentFlag.AlignCenter, self.tabText(self.drag_tab_index))
painter.end()
drag.setPixmap(pixmap)
layout.addWidget(header) tab_rect = self.tabRect(self.drag_tab_index)
container.setLayout(layout) if tab_rect.isValid():
drag.setHotSpot(event.pos() - tab_rect.topLeft())
else:
drag.setHotSpot(event.pos()) # Fallback hotspot
# Add to tab widget drop_action = drag.exec(Qt.DropAction.MoveAction)
index = super().addTab(container, title)
self.setTabText(index, title)
return index
def _close_tab(self, index: int): if drop_action == Qt.DropAction.IgnoreAction:
if index >= 0: self.tabDetached.emit(self.drag_tab_index, event.globalPosition().toPoint())
widget = self.widget(index)
web_view = widget.findChild(QWebEngineView) self.drag_tab_index = -1
if web_view: super().mouseMoveEvent(event)
web_view.deleteLater()
self.removeTab(index) def dragEnterEvent(self, event):
widget.deleteLater() if event.mimeData().hasFormat("application/x-qbrowser-tab-index"):
event.acceptProposedAction()
class BrowserApp(QMainWindow): else:
def __init__(self): super().dragEnterEvent(event)
def dropEvent(self, event):
if not event.mimeData().hasFormat("application/x-qbrowser-tab-index"):
return super().dropEvent(event)
source_tab_bar = event.source()
if not isinstance(source_tab_bar, DetachableTabBar):
return super().dropEvent(event)
source_widget = source_tab_bar.parent()
dest_widget = self.parent()
from_index = int(event.mimeData().data("application/x-qbrowser-tab-index"))
to_index = self.tabAt(event.position().toPoint())
if source_widget == dest_widget:
if to_index == -1:
to_index = self.count()
self.moveTab(from_index, to_index)
self.parent().setCurrentIndex(to_index)
event.acceptProposedAction()
return
# Cross-widget drop
tab_content = source_widget.widget(from_index)
tab_title = source_widget.tabText(from_index)
# Disconnect signals from the old browser
old_browser = tab_content.browser
if old_browser:
try:
tab_content.urlChanged.disconnect(old_browser.update_url)
tab_content.titleChanged.disconnect()
tab_content.loadStatusChanged.disconnect(old_browser.update_status_bar)
except (TypeError, RuntimeError):
pass
# Remove from source widget. The page widget is not deleted.
source_widget.removeTab(from_index)
if to_index == -1:
to_index = self.count()
# Add to destination widget
new_index = dest_widget.insertTab(to_index, tab_content, tab_title)
dest_widget.setCurrentIndex(new_index)
# Connect signals to the new browser
new_browser = dest_widget.window()
tab_content.browser = new_browser
tab_content.urlChanged.connect(new_browser.update_url)
tab_content.titleChanged.connect(lambda title, tab=tab_content: new_browser.update_title(title, tab))
tab_content.loadStatusChanged.connect(new_browser.update_status_bar)
# Close the source window if it has no more tabs
if source_widget.count() == 0:
source_widget.window().close()
event.setDropAction(Qt.DropAction.MoveAction)
event.accept()
class ProfileDialog(QDialog):
"""
Dialog for selecting or creating a browser profile.
"""
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Select Profile")
self.setMinimumWidth(400)
self.selected_profile_path = None
self.profiles_dir = "data"
if not os.path.exists(self.profiles_dir):
os.makedirs(self.profiles_dir)
layout = QVBoxLayout(self)
layout.addWidget(QLabel("Select a profile to launch:"))
self.profile_list = QListWidget()
self.profile_list.itemDoubleClicked.connect(self.accept_selection)
layout.addWidget(self.profile_list)
button_layout = QHBoxLayout()
self.new_button = QPushButton("Create New")
self.new_button.clicked.connect(self.create_new_profile)
button_layout.addWidget(self.new_button)
self.delete_button = QPushButton("Delete")
self.delete_button.clicked.connect(self.delete_profile)
button_layout.addWidget(self.delete_button)
button_layout.addStretch()
self.select_button = QPushButton("Launch")
self.select_button.setDefault(True)
self.select_button.clicked.connect(self.accept_selection)
button_layout.addWidget(self.select_button)
layout.addLayout(button_layout)
self.load_profiles()
self.update_button_states()
def update_button_states(self):
has_selection = len(self.profile_list.selectedItems()) > 0
self.select_button.setEnabled(has_selection)
self.delete_button.setEnabled(has_selection)
def load_profiles(self):
self.profile_list.clear()
profiles = []
for item in os.listdir(self.profiles_dir):
if os.path.isdir(os.path.join(self.profiles_dir, item)) and item.startswith("browser_profile_"):
profiles.append(item)
for profile_dir_name in sorted(profiles):
profile_name = profile_dir_name.replace("browser_profile_", "")
list_item = QListWidgetItem(profile_name)
list_item.setData(Qt.ItemDataRole.UserRole, os.path.join(self.profiles_dir, profile_dir_name))
self.profile_list.addItem(list_item)
if self.profile_list.count() > 0:
self.profile_list.setCurrentRow(0)
self.profile_list.itemSelectionChanged.connect(self.update_button_states)
self.update_button_states()
def create_new_profile(self):
while True:
profile_name, ok = QInputDialog.getText(self, "Create New Profile", "Enter a name for the new profile:")
if not ok:
return # User cancelled
if not profile_name.strip():
QMessageBox.warning(self, "Invalid Name", "Profile name cannot be empty.")
continue
profile_path = os.path.join(self.profiles_dir, f"browser_profile_{profile_name.strip()}")
if not os.path.exists(profile_path):
try:
os.makedirs(profile_path)
self._install_default_extensions(profile_path)
self.load_profiles()
# Select the new profile
for i in range(self.profile_list.count()):
if self.profile_list.item(i).text() == profile_name.strip():
self.profile_list.setCurrentRow(i)
break
return
except OSError as e:
QMessageBox.critical(self, "Error", f"Could not create profile directory: {e}")
return
else:
QMessageBox.warning(self, "Profile Exists", "A profile with that name already exists.")
def delete_profile(self):
selected_items = self.profile_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
profile_name = item.text()
profile_path = item.data(Qt.ItemDataRole.UserRole)
reply = QMessageBox.question(self, "Confirm Deletion", f"Are you sure you want to delete the profile '{profile_name}'?\nThis will permanently delete all its data.",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No)
if reply == QMessageBox.StandardButton.Yes:
try:
shutil.rmtree(profile_path)
self.load_profiles()
self.update_button_states()
except Exception as e:
QMessageBox.critical(self, "Error", f"Could not delete profile: {e}")
def accept_selection(self):
selected_items = self.profile_list.selectedItems()
if not selected_items:
return
item = selected_items[0]
self.selected_profile_path = item.data(Qt.ItemDataRole.UserRole)
self.accept()
@staticmethod
def _install_default_extensions(profile_path):
"""Copies default extensions from assets to a new profile directory."""
source_extensions_dir = "assets/browser/extensions"
if not os.path.isdir(source_extensions_dir):
print(f"Default extensions directory not found: {source_extensions_dir}")
return
dest_extensions_dir = os.path.join(profile_path, "Default", "Extensions")
try:
os.makedirs(dest_extensions_dir, exist_ok=True)
for item_name in os.listdir(source_extensions_dir):
source_item_path = os.path.join(source_extensions_dir, item_name)
dest_item_path = os.path.join(dest_extensions_dir, item_name)
if os.path.isdir(source_item_path):
if not os.path.exists(dest_item_path):
shutil.copytree(source_item_path, dest_item_path)
print(f"Installed default extension '{item_name}' to profile.")
except (OSError, IOError) as e:
QMessageBox.warning(None, "Extension Installation Error",
f"Could not install default extensions: {e}")
@staticmethod
def get_profile_path(parent=None):
profiles_dir = "data"
if not os.path.exists(profiles_dir):
os.makedirs(profiles_dir)
profiles = [p for p in os.listdir(profiles_dir) if p.startswith("browser_profile_")]
if not profiles:
# First run experience: must create a profile to continue.
while True:
profile_name, ok = QInputDialog.getText(parent, "Create First Profile", "Welcome! Please create a profile to begin:")
if not ok:
return None # User cancelled initial creation, so exit.
if not profile_name.strip():
QMessageBox.warning(parent, "Invalid Name", "Profile name cannot be empty.")
continue
profile_path = os.path.join(profiles_dir, f"browser_profile_{profile_name.strip()}")
if os.path.exists(profile_path):
QMessageBox.warning(parent, "Profile Exists", "A profile with that name already exists. Please choose another name.")
continue
try:
os.makedirs(profile_path)
ProfileDialog._install_default_extensions(profile_path)
return profile_path # Success!
except OSError as e:
QMessageBox.critical(parent, "Error", f"Could not create profile directory: {e}")
return None
else:
# Profiles exist, show the manager dialog.
dialog = ProfileDialog(parent)
if dialog.exec() == QDialog.DialogCode.Accepted:
return dialog.selected_profile_path
else:
return None # User closed the manager.
class Browser(QMainWindow):
"""
Main browser window that wraps the Playwright browser class.
"""
instances = []
def __init__(self, initial_url=None, debug=False, detached_tab=None, profile_path=None):
super().__init__() super().__init__()
self.setWindowTitle("Embedded Chromium Tabs") Browser.instances.append(self)
self.setGeometry(100, 100, 800, 600) self.setWindowTitle("SHMCamStudio Browser")
self.setWindowIcon(QIcon('assets/logo.jpg'))
self.resize(1024, 768)
# Create a central widget and layout
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout(self.central_widget)
self.layout.setContentsMargins(0, 0, 0, 0)
self.layout.setSpacing(0)
# Create toolbar
self.toolbar = QToolBar()
self.toolbar.setMovable(False)
self.toolbar.setIconSize(QSize(24, 24))
# Add back button
self.back_button = QPushButton()
self.back_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowBack))
self.back_button.setToolTip("Back")
self.back_button.clicked.connect(self.navigate_back)
self.toolbar.addWidget(self.back_button)
# Add forward button
self.forward_button = QPushButton()
self.forward_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_ArrowForward))
self.forward_button.setToolTip("Forward")
self.forward_button.clicked.connect(self.navigate_forward)
self.toolbar.addWidget(self.forward_button)
# Add reload button
self.reload_button = QPushButton()
self.reload_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_BrowserReload))
self.reload_button.setToolTip("Reload (F5)")
self.reload_button.clicked.connect(self.reload_page)
self.toolbar.addWidget(self.reload_button)
# Add home button
self.home_button = QPushButton()
self.home_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_DirHomeIcon))
self.home_button.setToolTip("Home")
self.home_button.clicked.connect(self.navigate_home)
self.toolbar.addWidget(self.home_button)
# Add URL input
self.url_input = QLineEdit()
self.url_input.setPlaceholderText("Enter URL...")
self.url_input.returnPressed.connect(self.navigate_to_url)
self.toolbar.addWidget(self.url_input)
# The extension buttons will be inserted before this one, so we need its action.
self.extensions_button = QPushButton()
self.extensions_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileDialogDetailedView))
self.extensions_button.setToolTip("Extensions")
self.extensions_button.clicked.connect(self.show_extensions)
self.extensions_button_action = self.toolbar.addWidget(self.extensions_button)
# Add new window button
self.new_window_button = QPushButton()
self.new_window_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_FileIcon))
self.new_window_button.setToolTip("New Window (Ctrl+W)")
self.new_window_button.clicked.connect(self.open_new_window)
self.toolbar.addWidget(self.new_window_button)
if debug:
self.debug_button = QPushButton()
self.debug_button.setIcon(self._get_themed_icon(QStyle.StandardPixmap.SP_MessageBoxWarning))
self.debug_button.setToolTip("Toggle Developer Tools")
self.debug_button.clicked.connect(self.toggle_devtools)
self.toolbar.addWidget(self.debug_button)
self.layout.addWidget(self.toolbar)
# Create tab widget # Create tab widget
self.tab_widget = CloseableTabWidget(self) self.tabs = QTabWidget()
self.setCentralWidget(self.tab_widget) self.tab_bar = DetachableTabBar()
self.tabs.setTabBar(self.tab_bar)
self.tab_bar.tabDetached.connect(self.handle_tab_detached)
self.tabs.setTabsClosable(True)
# self.tabs.setMovable(True) # This is handled by our DetachableTabBar
self.tabs.setDocumentMode(True)
self.tabs.tabCloseRequested.connect(self.close_tab)
self.tabs.currentChanged.connect(self.tab_changed)
self.layout.addWidget(self.tabs)
# Add status bar
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
# Add a new tab button
self.tabs.setCornerWidget(self._create_new_tab_button(), Qt.Corner.TopRightCorner)
# Set the home URL from config
config = configparser.ConfigParser()
config.read('shmcamstudio.conf')
self.home_url = config.get('Browser', 'home_url', fallback='https://www.google.com')
# Store browser instances, views, and pages # Centralized browser profile
self.browsers: List[Browser] = [] if not profile_path:
self.web_views: List[QWebEngineView] = [] # This should not happen if main is used, but as a fallback
self.pages: List[Page] = [] profile_path = "data/browser_profile_default"
self.contexts: List[BrowserContext] = [] self.profile_dir = profile_path
if not os.path.exists(self.profile_dir):
os.makedirs(self.profile_dir)
# Extensions must be in a specific subdirectory of the profile
self.extensions_dir = os.path.join(self.profile_dir, "Default", "Extensions")
if not os.path.exists(self.extensions_dir):
os.makedirs(self.extensions_dir)
self.profile = QWebEngineProfile(self.profile_dir, self)
self.extension_scheme_handler = ExtensionSchemeHandler(self.extensions_dir)
self.profile.installUrlSchemeHandler(b"qextension", self.extension_scheme_handler)
if detached_tab:
widget, title = detached_tab
index = self.tabs.addTab(widget, title)
self.tabs.setCurrentIndex(index)
widget.browser = self
widget.urlChanged.connect(self.update_url)
widget.titleChanged.connect(lambda t, tab=widget: self.update_title(t, tab))
widget.loadStatusChanged.connect(self.update_status_bar)
if hasattr(widget, 'web_view'):
self.update_title(widget.web_view.title(), widget)
self.update_url(widget.web_view.url().toString())
else:
# Create a new tab on startup
url_to_open = initial_url if initial_url is not None else self.home_url
self.new_page(url_to_open)
# Set up keyboard shortcuts
self._setup_shortcuts()
# Store Playwright objects
self.playwright = None
self.playwright_browser = None
self.browser_contexts = {}
self.dev_tools_windows = []
self.open_extension_popups = []
self.extension_actions = []
self.update_extension_buttons()
def closeEvent(self, event):
if self in Browser.instances:
Browser.instances.remove(self)
super().closeEvent(event)
def _get_themed_icon(self, standard_pixmap, color="white"):
"""Create a themed icon from a standard pixmap."""
icon = self.style().standardIcon(standard_pixmap)
pixmap = icon.pixmap(self.toolbar.iconSize())
painter = QPainter(pixmap)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
painter.fillRect(pixmap.rect(), QColor(color))
painter.end()
return QIcon(pixmap)
def _create_new_tab_button(self):
"""Create a button for adding new tabs."""
button = QPushButton("+")
button.setToolTip("New Tab")
button.clicked.connect(lambda: self.new_page(self.home_url))
return button
def _setup_shortcuts(self):
"""Set up keyboard shortcuts."""
# Ctrl+T: New Tab
new_tab_shortcut = QAction("New Tab", self)
new_tab_shortcut.setShortcut(QKeySequence("Ctrl+T"))
new_tab_shortcut.triggered.connect(lambda: self.new_page(self.home_url))
self.addAction(new_tab_shortcut)
# Ctrl+Tab: Cycle Tabs
cycle_tabs_shortcut = QAction("Cycle Tabs", self)
cycle_tabs_shortcut.setShortcut(QKeySequence("Ctrl+Tab"))
cycle_tabs_shortcut.triggered.connect(self.cycle_tabs)
self.addAction(cycle_tabs_shortcut)
# Ctrl+W: New Window
new_window_shortcut = QAction("New Window", self)
new_window_shortcut.setShortcut(QKeySequence("Ctrl+W"))
new_window_shortcut.triggered.connect(self.open_new_window)
self.addAction(new_window_shortcut)
# Ctrl+L: Focus URL bar
focus_url_shortcut = QAction("Focus URL", self)
focus_url_shortcut.setShortcut(QKeySequence("Ctrl+L"))
focus_url_shortcut.triggered.connect(self.url_input.setFocus)
self.addAction(focus_url_shortcut)
# F5: Reload page
reload_shortcut = QAction("Reload", self)
reload_shortcut.setShortcut(QKeySequence("F5"))
reload_shortcut.triggered.connect(self.reload_page)
self.addAction(reload_shortcut)
def cycle_tabs(self):
"""Cycle through the open tabs."""
if self.tabs.count() > 1:
next_index = (self.tabs.currentIndex() + 1) % self.tabs.count()
self.tabs.setCurrentIndex(next_index)
def on_tab_removed(self):
"""Close the window if the last tab is removed."""
if self.tabs.count() == 0:
self.close()
def handle_tab_detached(self, index, pos):
"""Handle a tab being detached to a new window."""
if index < 0 or index >= self.tabs.count():
return
# Asyncio event loop widget = self.tabs.widget(index)
self.loop = asyncio.get_event_loop() title = self.tabs.tabText(index)
def add_tab(self, url: str, page: Page, context: BrowserContext, browser: Browser, tab_idx: int): try:
# Create QWebEngineView for the tab widget.urlChanged.disconnect(self.update_url)
widget.titleChanged.disconnect()
widget.loadStatusChanged.disconnect(self.update_status_bar)
except (TypeError, RuntimeError):
pass
# Create a new browser instance, which will take ownership of the widget.
new_browser = Browser(
debug=("--debug" in sys.argv),
detached_tab=(widget, title),
profile_path=self.profile_dir
)
new_browser.setGeometry(self.geometry())
new_browser.move(pos - QPoint(new_browser.width() // 4, 10))
new_browser.show()
# Now that the widget is safely reparented, remove the tab from the old browser.
# The on_tab_removed method will handle closing the window if it's empty.
self.tabs.removeTab(index)
self.on_tab_removed()
def new_page(self, url="about:blank"):
"""Create a new browser tab with the specified URL."""
# Create a new tab
tab = BrowserTab(parent=self.tabs, url=url, profile=self.profile)
# Store a reference to the Browser instance
tab.browser = self
tab.urlChanged.connect(self.update_url)
tab.titleChanged.connect(lambda title, tab=tab: self.update_title(title, tab))
tab.loadStatusChanged.connect(self.update_status_bar)
# Add the tab to the tab widget
index = self.tabs.addTab(tab, "New Tab")
self.tabs.setCurrentIndex(index)
return tab
def close_tab(self, index):
"""Close the tab at the specified index."""
if index < 0 or index >= self.tabs.count():
return
# Get the tab widget and schedule it for deletion.
tab = self.tabs.widget(index)
if tab:
tab.deleteLater()
# Remove the tab from the tab bar.
self.tabs.removeTab(index)
self.on_tab_removed()
def tab_changed(self, index):
"""Handle tab change events."""
if index < 0:
return
# Update the URL input
tab = self.tabs.widget(index)
if hasattr(tab, 'url'):
self.url_input.setText(tab.url)
self.update_status_bar()
def update_url(self, url):
"""Update the URL input when the page URL changes."""
self.url_input.setText(url)
# Update the tab's stored URL
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.url = url
def update_title(self, title, tab):
"""Update the tab title when the page title changes."""
index = self.tabs.indexOf(tab)
if index >= 0:
self.tabs.setTabText(index, title or "New Tab")
def navigate_to_url(self):
"""Navigate to the URL in the URL input."""
url = self.url_input.text().strip()
# Add http:// if no protocol is specified
if url and not url.startswith(("http://", "https://", "file://", "about:")):
url = "http://" + url
# Navigate the current tab
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.navigate(url)
def navigate_home(self):
"""Navigate to the home URL."""
current_tab = self.tabs.currentWidget()
if current_tab:
current_tab.navigate(self.home_url)
def navigate_back(self):
"""Navigate back in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.back()
def navigate_forward(self):
"""Navigate forward in the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.forward()
def reload_page(self):
"""Reload the current tab."""
current_tab = self.tabs.currentWidget()
if current_tab and hasattr(current_tab, 'web_view'):
current_tab.web_view.reload()
def open_new_window(self):
"""Open a new browser window."""
new_browser = Browser(debug="--debug" in sys.argv, profile_path=self.profile_dir)
new_browser.show()
def toggle_devtools(self):
"""Opens the remote developer tools URL in a new window."""
dev_tools_window = QDialog(self)
dev_tools_window.setWindowTitle("Developer Tools")
dev_tools_window.resize(1200, 800)
layout = QVBoxLayout()
web_view = QWebEngineView() web_view = QWebEngineView()
web_view.setUrl(QUrl(url)) web_view.load(QUrl("http://localhost:9222"))
self.tab_widget.addTab(web_view, f"Tab {tab_idx+1}") layout.addWidget(web_view)
self.web_views.append(web_view)
self.pages.append(page) dev_tools_window.setLayout(layout)
self.contexts.append(context) dev_tools_window.show()
self.browsers.append(browser)
self.dev_tools_windows.append(dev_tools_window)
dev_tools_window.finished.connect(lambda: self.dev_tools_windows.remove(dev_tools_window))
# Sync Playwright with QWebEngineView def update_status_bar(self):
QTimer.singleShot(1000, lambda: self.loop.create_task(self._sync_viewport(page, web_view, tab_idx))) """Update the status bar based on the current tab's state."""
current_tab = self.tabs.currentWidget()
if not current_tab:
self.status_bar.showMessage("Ready")
return
if current_tab.is_loading:
self.status_bar.showMessage("Loading...")
else:
self.status_bar.showMessage("Ready")
def reload_profile_and_tabs(self):
"""Reloads the browser profile and recreates pages in all tabs."""
print("Reloading profile to apply extension changes...")
# 1. Save the URLs and current index of all open tabs
urls = []
current_index = self.tabs.currentIndex()
for i in range(self.tabs.count()):
tab = self.tabs.widget(i)
if hasattr(tab, 'web_view') and tab.web_view.url().isValid():
urls.append(tab.web_view.url().toString())
else:
urls.append("about:blank")
# 2. Store the old profile to delete it later
old_profile = self.profile
old_profile.clearHttpCache()
# 3. Clear all existing tabs. This removes references to pages using the old profile.
self.tabs.clear()
# 4. Create a new profile object. This forces a re-read from disk because
# the old one will be properly deleted.
self.profile = QWebEngineProfile(self.profile_dir, self)
self.profile.installUrlSchemeHandler(b"qextension", self.extension_scheme_handler)
# 5. Re-create all tabs with the new profile
if not urls: # Ensure there's at least one tab
urls.append(self.home_url)
for url in urls:
self.new_page(url)
# 6. Restore the previously active tab
if current_index != -1 and current_index < len(urls):
self.tabs.setCurrentIndex(current_index)
# 7. Schedule the old profile for deletion. This is crucial.
old_profile.setParent(None)
old_profile.deleteLater()
# Register event handlers print("Profile and tabs reloaded.")
page.on("console", self._make_console_handler(page, tab_idx)) self.update_extension_buttons()
context.on("page", self._make_page_handler(tab_idx))
def open_extension_popup_from_toolbar(self, ext_path):
"""Opens an extension's popup from a toolbar button click."""
ext_name = os.path.basename(ext_path)
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
QMessageBox.warning(self, "Error", "manifest.json not found for this extension.")
return
async def _sync_viewport(self, page: Page, web_view: QWebEngineView, tab_idx: int):
try: try:
width = web_view.width() with open(manifest_path, 'r', encoding='utf-8') as f:
height = web_view.height() manifest = json.load(f)
await page.set_viewport_size({"width": width, "height": height})
print(f"Tab {tab_idx+1}: Synced viewport to {width}x{height}")
except Exception as e: except Exception as e:
print(f"Tab {tab_idx+1}: Error syncing viewport: {e}") QMessageBox.critical(self, "Error", f"Could not read manifest.json: {e}")
return
def _make_console_handler(self, page: Page, tab_idx: int): popup_path = None
async def on_console(msg): action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if msg.text.startswith("Viewport resized:"): if action and action.get('default_popup'):
try: popup_path = action.get('default_popup')
viewport_data = json.loads(msg.text.replace("Viewport resized:", ""))
width = int(viewport_data["width"])
height = int(viewport_data["height"])
pixel_ratio = viewport_data["devicePixelRatio"]
print(f"Tab {tab_idx+1}: Real viewport size: {width}x{height} (DPR: {pixel_ratio})")
await page.set_viewport_size({"width": width, "height": height})
except Exception as e:
print(f"Tab {tab_idx+1}: Error parsing viewport data: {e}")
return on_console
def _make_page_handler(self, tab_idx: int):
async def on_new_page(new_page: Page):
print(f"Tab {tab_idx+1}: New page created")
new_url = await new_page.evaluate("window.location.href")
new_web_view = QWebEngineView()
new_web_view.setUrl(QUrl(new_url))
new_tab_idx = len(self.web_views)
self.tab_widget.addTab(new_web_view, f"Tab {new_tab_idx+1}")
self.web_views.append(new_web_view)
self.pages.append(new_page)
new_page.on("console", self._make_console_handler(new_page, tab_idx))
self.loop.create_task(self._sync_viewport(new_page, new_web_view, tab_idx))
return on_new_page
def _on_resize(self, event):
for i, (page, web_view) in enumerate(zip(self.pages, self.web_views)):
if self.tab_widget.currentIndex() == i or len(self.web_views) == 1:
self.loop.create_task(self._sync_viewport(page, web_view, i))
def closeEvent(self, event): if not popup_path:
for browser in self.browsers: # This should not happen if the button was created, but as a safeguard:
self.loop.create_task(browser.close()) QMessageBox.information(self, "No Popup", "This extension does not have a popup.")
event.accept() return
popup_file = os.path.join(ext_path, popup_path)
if not os.path.exists(popup_file):
QMessageBox.warning(self, "Error", f"Popup file not found: {popup_path}")
return
# Open the popup in a new non-modal dialog
popup_dialog = QDialog(self)
popup_dialog.setWindowTitle(manifest.get("name", "Extension Popup"))
layout = QVBoxLayout(popup_dialog)
web_view = QWebEngineView()
# The 'self' here is the Browser instance, so self.profile is correct
page = QWebEnginePage(self.profile, web_view)
web_view.setPage(page)
popup_url = QUrl(f"qextension://{ext_name}/{popup_path}")
web_view.load(popup_url)
layout.addWidget(web_view)
popup_dialog.setLayout(layout)
popup_dialog.resize(400, 600)
popup_dialog.show()
# Keep a reference to prevent garbage collection
self.open_extension_popups.append(popup_dialog)
popup_dialog.finished.connect(lambda: self.open_extension_popups.remove(popup_dialog))
def update_extension_buttons(self):
"""Scans for extensions and adds a button for each one with a popup to the main toolbar."""
# Remove previous extension buttons
for action in self.extension_actions:
self.toolbar.removeAction(action)
self.extension_actions.clear()
if not self.extensions_dir or not os.path.exists(self.extensions_dir):
return
# Find all enabled extension directories
for ext_name in sorted(os.listdir(self.extensions_dir)):
if ext_name.endswith(".disabled"):
continue
ext_path = os.path.join(self.extensions_dir, ext_name)
if not os.path.isdir(ext_path):
continue
manifest_path = os.path.join(ext_path, "manifest.json")
if not os.path.exists(manifest_path):
continue
try:
with open(manifest_path, 'r', encoding='utf-8') as f:
manifest = json.load(f)
except Exception as e:
print(f"Error loading manifest for {ext_name}: {e}")
continue
# Check for a popup action
popup_path = None
action = manifest.get('action') or manifest.get('browser_action') or manifest.get('page_action')
if action and action.get('default_popup'):
popup_path = action.get('default_popup')
class QtPlaywrightBrowser: if popup_path:
def __init__(self, playwright): # Create a button for this extension
self._playwright = playwright button = QPushButton()
self._app = QApplication.instance() or QApplication(sys.argv) button.setFlat(True)
self._window = BrowserApp() button.setIconSize(QSize(22, 22))
self._window.show()
self._loop = asyncio.get_event_loop() # Set the icon
self._contexts: List[BrowserContext] = [] icon_path = None
self._tab_idx = 0 if manifest.get("icons"):
# Prefer a 24px or 16px icon for the toolbar
async def new_context(self, **kwargs) -> BrowserContext: icon_path = manifest["icons"].get("24") or manifest["icons"].get("16") or next(iter(manifest["icons"].values()), None)
browser = await self._playwright.chromium.launch(
headless=False, if icon_path:
args=["--app=https://www.google.com"] full_icon_path = os.path.join(ext_path, icon_path)
if os.path.exists(full_icon_path):
button.setIcon(QIcon(full_icon_path))
button.setToolTip(manifest.get("name", ext_name))
button.clicked.connect(partial(self.open_extension_popup_from_toolbar, ext_path))
# Insert the button before the main "Extensions" button
action = self.toolbar.insertWidget(self.extensions_button_action, button)
self.extension_actions.append(action)
def show_extensions(self):
"""Show the extensions dialog."""
dialog = ExtensionDialog(self, self.extensions_dir)
dialog.exec()
class QPlaywrightBrowser:
"""
A class that wraps the Playwright browser API and uses Qt6Browser internally.
This class exposes the Playwright API while using the Qt6 browser for display.
"""
def __init__(self):
self.app = QApplication.instance() or QApplication(sys.argv)
self.browser_ui = None
self.playwright = None
self.browser = None
self.contexts = {}
self.event_loop = None
async def launch(self, **kwargs):
"""Launch a new browser instance."""
# Initialize Playwright if not already done
if not self.playwright:
self.playwright = await async_playwright().start()
# Create the Qt browser UI
if not self.browser_ui:
self.browser_ui = Browser()
self.browser_ui.show()
# Launch the actual Playwright browser (hidden)
# We'll use this for API compatibility but display in Qt
self.browser = await self.playwright.chromium.launch(
headless=True, # Run headless since we're displaying in Qt
**kwargs
) )
context = await browser.new_context(**kwargs)
self._contexts.append(context) # Create a default context
default_context = await self.browser.new_context()
# Inject JavaScript for resize events self.contexts["default"] = default_context
await context.add_init_script("""
let resizeTimeout; return self
window.addEventListener('resize', () => {
clearTimeout(resizeTimeout); async def new_context(self, **kwargs):
resizeTimeout = setTimeout(() => { """Create a new browser context."""
const viewportSize = { if not self.browser:
width: window.innerWidth, await self.launch()
height: window.innerHeight,
devicePixelRatio: window.devicePixelRatio # Create a new context in the Playwright browser
}; context_id = f"context_{len(self.contexts)}"
console.log('Viewport resized:', JSON.stringify(viewportSize)); context = await self.browser.new_context(**kwargs)
}, 100); self.contexts[context_id] = context
});
window.dispatchEvent(new Event('resize')); # Return a wrapper that provides both Playwright API and Qt UI
""") return QPlaywrightBrowserContext(self, context, context_id)
return context async def new_page(self, url="about:blank"):
"""Create a new page in the default context."""
async def new_page(self, url: str = "https://about:blank") -> Page: if "default" not in self.contexts:
context = self._contexts[-1] if self._contexts else await self.new_context() await self.launch()
for _ in range(10):
page = await context.new_page() # Create a new page in the Playwright browser
if page: pw_page = await self.contexts["default"].new_page()
break await pw_page.goto(url)
print(f"Tab {self._tab_idx+1}: Waiting for page to initialize...")
await asyncio.sleep(0.5) # Create a new tab in the Qt browser
else: qt_tab = self.browser_ui.new_page(url)
raise RuntimeError(f"Failed to initialize page for {url}") qt_tab.page = pw_page
# Return a wrapper that provides both Playwright API and Qt UI
return QPlaywrightPage(pw_page, qt_tab)
async def close(self):
"""Close the browser."""
# Close all Playwright contexts and browser
if self.browser:
for context_id, context in self.contexts.items():
await context.close()
await self.browser.close()
self.contexts = {}
self.browser = None
# Close the Qt browser UI
if self.browser_ui:
self.browser_ui.close()
self.browser_ui = None
# Close Playwright
if self.playwright:
await self.playwright.stop()
self.playwright = None
def run(self):
"""Run the application event loop."""
return self.app.exec()
def run_async(self, coro):
"""Run an async coroutine in the Qt event loop."""
if not self.event_loop:
self.event_loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.event_loop)
# Create a QTimer to process asyncio events
timer = QTimer()
timer.timeout.connect(lambda: self.event_loop.run_until_complete(asyncio.sleep(0)))
timer.start(10) # 10ms interval
# Run the coroutine
future = asyncio.run_coroutine_threadsafe(coro, self.event_loop)
return future
await page.goto(url)
await page.set_viewport_size({"width": 800, "height": 600}) class QPlaywrightBrowserContext:
self._window.add_tab(url, page, context, context.browser, self._tab_idx) """
self._tab_idx += 1 A wrapper around a Playwright BrowserContext that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, qbrowser, context, context_id):
self.qbrowser = qbrowser
self.context = context
self.context_id = context_id
self.pages = []
async def new_page(self):
"""Create a new page in this context."""
# Create a new page in the Playwright context
pw_page = await self.context.new_page()
# Create a new tab in the Qt browser
qt_tab = self.qbrowser.browser_ui.new_page("about:blank")
qt_tab.page = pw_page
# Create a wrapper page
page = QPlaywrightPage(pw_page, qt_tab)
self.pages.append(page)
return page return page
async def close(self):
"""Close this context."""
# Close all pages
for page in self.pages:
await page.close()
# Close the Playwright context
await self.context.close()
# Remove from the qbrowser contexts
if self.context_id in self.qbrowser.contexts:
del self.qbrowser.contexts[self.context_id]
class QPlaywrightPage:
"""
A wrapper around a Playwright Page that provides both
Playwright API and Qt UI integration.
"""
def __init__(self, pw_page, qt_tab):
self.pw_page = pw_page
self.qt_tab = qt_tab
# Forward Playwright page methods and properties
self.goto = pw_page.goto
self.click = pw_page.click
self.fill = pw_page.fill
self.type = pw_page.type
self.press = pw_page.press
self.wait_for_selector = pw_page.wait_for_selector
self.wait_for_navigation = pw_page.wait_for_navigation
self.wait_for_load_state = pw_page.wait_for_load_state
self.evaluate = pw_page.evaluate
self.screenshot = pw_page.screenshot
self.content = pw_page.content
self.title = pw_page.title
self.url = pw_page.url
async def close(self): async def close(self):
for context in self._contexts: """Close this page."""
await context.close() # Close the Playwright page
self._window.close() await self.pw_page.close()
# Allow Qt event loop to process close events
QTimer.singleShot(0, self._app.quit) # Close the Qt tab
# We need to use the Qt event loop for this
index = self.qt_tab.parent.indexOf(self.qt_tab)
if index >= 0:
self.qt_tab.parent.close_tab(index)
@property
def contexts(self) -> List[BrowserContext]:
return self._contexts
def __aenter__(self): async def main_async():
return self """Async main function to run the browser."""
browser = QPlaywrightBrowser()
await browser.launch()
# Open a page
url = "https://www.google.com"
if len(sys.argv) > 1:
url = sys.argv[1]
await browser.new_page(url)
# Keep the browser running
while True:
await asyncio.sleep(0.1)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.close()
def main(): def main():
async def run(): """Main function to run the browser as a standalone application."""
async with async_playwright() as p: debug_mode = "--debug" in sys.argv
browser = QtPlaywrightBrowser(p) if debug_mode:
page = await browser.new_page("https://example.com") os.environ["QTWEBENGINE_REMOTE_DEBUGGING"] = "9222"
page2 = await browser.new_page("https://www.google.com")
page3 = await browser.new_page("https://www.wikipedia.org") app = QApplication.instance() or QApplication(sys.argv)
await page.evaluate("window.open('https://www.bing.com')")
await asyncio.sleep(60) # Keep GUI open # Allow Ctrl+C to kill the application gracefully
await browser.close() signal.signal(signal.SIGINT, signal.SIG_DFL)
app.setWindowIcon(QIcon('assets/logo.jpg'))
asyncio.run(run())
# Set a modern, dark theme
if __name__ == '__main__': app.setStyle("Fusion")
main() dark_palette = app.palette()
dark_palette.setColor(dark_palette.ColorRole.Window, QColor(45, 45, 45))
dark_palette.setColor(dark_palette.ColorRole.WindowText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Base, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.AlternateBase, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ToolTipBase, QColor(25, 25, 25))
dark_palette.setColor(dark_palette.ColorRole.ToolTipText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Text, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.Button, QColor(53, 53, 53))
dark_palette.setColor(dark_palette.ColorRole.ButtonText, Qt.GlobalColor.white)
dark_palette.setColor(dark_palette.ColorRole.BrightText, Qt.GlobalColor.red)
dark_palette.setColor(dark_palette.ColorRole.Link, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.Highlight, QColor(42, 130, 218))
dark_palette.setColor(dark_palette.ColorRole.HighlightedText, Qt.GlobalColor.black)
app.setPalette(dark_palette)
# Set a better font
font = QFont("Cantarell", 10)
if platform.system() == "Windows":
font = QFont("Segoe UI", 10)
elif platform.system() == "Darwin":
font = QFont("San Francisco", 10)
app.setFont(font)
# Create a dummy parent for dialogs to ensure they are properly modal
dummy_parent = QWidget()
# Get profile path
profile_path = ProfileDialog.get_profile_path(dummy_parent)
if not profile_path:
return 0 # Exit gracefully
# Create a simple browser directly without Playwright for now
# This ensures we at least get a window showing
initial_url = None
if len(sys.argv) > 1 and not sys.argv[1].startswith('--'):
initial_url = sys.argv[1]
browser = Browser(initial_url=initial_url, debug=debug_mode, profile_path=profile_path)
browser.show()
print("Browser window should be visible now")
# Run the Qt event loop
return app.exec()
if __name__ == "__main__":
sys.exit(main())
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment