Hunting Down Audio Bugs – a view from behind the scenes

All what should become a great mess later started with a single bug report some time back:

“[SlowAudio] HTML5 Live stream has bad sound and video”

(dramatic music effect)
found by Gwendragon, who in the following events tested, logged and categorized a lot of audio stuff too.

This bug happened only on a single site, which is well frequented in one country, but otherwise rather irrelevant.
Back then we could only check if it works for us – which it did for those who tested it after spoofing the user agent – so this bug was set to “cannot reproduce” (which means: Closed) after contacting the site owner because at that time it appeared to be a problem for the site to fix.

Usually this is the end of the story, the developers can only fix what they see and what they can analyze, and if it works for the testers and the developers, it can’t be broken, right?

Sadly not.

Somehow we managed to forget one operating system (OS) during the check, which was partly my fault, because usually I use that OS while testing.
All was good until the next report came in for exactly the same site. We checked it again (usual procedure, no bug gets closed unchecked), this time with the right OS too and the result was: [insert some heavy cussing and swearing here – be creative, ’cause I was]!

Not only that – suddenly reports about dozens of other sites started popping up!

PANIC!!!

Well, no panic, but no real solution in sight either, which is quite frustrating because at that time all of the developers, who can fix this kind of bugs, were heavily burdened with really important stuff like frozen UI or crashes, which of course have a higher priority, especially because super traffic-heavy sites like YT, Vimeo etc. worked well – but of course those audio bugs were constantly nagging in the background.

Then, one day, it was finally possible to assign a developer to the problem, but the fix was everything but easy.

One of the first things was to find out where it breaks and what exactly breaks – which sounds easy enough but isn’t, especially if you have to deal with the codecs inside of the OS because prohibitive license fees from the MPEG-LA and the Fraunhofer institute forbid to bundle the full ffmpeg+aac codec in binary form to Vivaldi, no thanks to them.
(Linux users might have noticed by now, that they are not hit by this because they can steal re-purpose the original Chrome codecs hosted in the usual repositories – a luxury the owners of other OSs don’t have)

The first thing to do was to add some sophisticated debugging code to the internal builds which can provide log files for the sites that were affected by the issue.
Said and done, we testers (both the employed and we volunteers) went through all of the bugs and created tons of big log files which we dumped on the poor Developer. After some time some of us became experienced enough to identify some common problems so that we could “Duplicate” bugs (meaning link together bugs that have the identical cause) on some kind of master bugs (usually the bug where the exact problem was first seen).

One thing all of those bugs had in common was the interaction between Vivaldi and the OS codecs. First Vivaldi needed to parse out the correct information from the media stream to get the correct settings for the audio decoder, then it needed to deliver it to the OS in exactly the right way or everything would break, which it still did at that time. This meant digging deep into the OS – one of the developers even went so far to read through The Windows Bible (a set of 1k+ page books that describe the internal workings in detail) to find “the right MS way(TM)” to do that.

This resulted in the first set of patches, or, to be more precise: One single patch which fixed a lot of the problems that existed.
(Here my part of hero worshiping: Patricia did this patch “blind” – i.e. without working on the OS directly! And it worked on the first try! That is quite a huge feat!)

End of story … not …

… because this fixed only the most visible bugs, or audible, in this case.

There was a different bunch of similar bugs – which audibly sounded the same but internally weren’t – we saw that they were there but at that point we could not set see what exactly caused it, so back to writing log files, but this time with the improved “media bug logger 3000(TM)” to get even more data and again we dumped it on the developer: Set me on the watch-list for ALL of those bugs! (slightly misquoted, but that was the essence of what Patricia said)

Those bugs were related with the way the servers send the data. To get a grip on those, the log files alone were not enough, because they told us only what the browser thinks it has seen – which might not be and was not always the same as is really there.

Side note: To understand that one must know that the involved AAC files can be delivered in multiple ways: With different profiles, with the profile announcing what is inside (the easier case), some with implicit announcement (which means we need to look inside of the data), some without any headers at all and some of those even encrypted (more about those later).

We had to find a way to get the raw “files” as delivered by the server. Sadly they are not delivered as files, but in little chunks that temporarily exist in the RAM. Downloading the requested data with the dev tools was no option1, so Tarquin came up with the genius idea to grab the stuff with an extension (no, even that will not give you a working file, don’t bother to ask for a downloader. I can hear your thoughts from here, the extension doesn’t work that way 😛 ) and finally we were able to “download” small bunches of the raw data, but when we tried to look into those, a lot of what was inside looked like a garbled mess. We are no audio decoding experts, usually the codecs “just do it” and we all hope they do it right, so we definitely needed to wise up, quick.

At some point, when I got totally frustrated about that pile of horse manure some servers delivered, I remembered that I have a friend who “is into that codec stuff”. Usually SagaraS works with video codecs and does all kinds of fancy things with them to improve the output quality but hey, it was worth a shot. So I dumped some of the grabbed files we couldn’t analyze at that point on him and asked him to find out what all of that stuff means. It took him some hours or so to get a grip on the audio stuff. When he tried to explain to me what it does and how we can analyze it – in purest AV Geek language 2, it could have been Swahili and I couldn’t have told the difference – my mind boggled. In the end we set up a teamviewer session the next day so that I could look over his shoulder while he showed me what to watch out for and how to do it. It wasn’t that easy, he is armed with an Hex Editor and knows how to use it, but he gave us the necessary clues. Additionally he pointed us to tools we can use to analyze that stuff, which was of great help too.

So back to logging and grabbing and analyzing it was, while the developers started fixing the next bunch of issues, this time in a teamed effort between Patricia and Julien because of the sheer amount, resulting in what you now know as More HTML5 audio fixes – Vivaldi Browser snapshot 1.15.1147.23.

End of story? Not quite …

Some web pages strictly resisted to give us meaningful content to grab with the above mentioned extension: All we got was seriously garbled junk.

In the end we had to dive deep into the code of the website player – cough, not really “we”, the developer of the extension. I could only confirm from the code (which was a PITA of its own: >60k lines of JavaScript is a lot to look through) that they encrypt the stuff on the server and decrypt it in the browser on the fly and dynamically add the header to it, but not what it exactly does. Finally the Tarquin found the exact place where to grab the necessary information needed for the decryption module, so be prepared for the next bunch of fixes.

Oh, btw: While doing all of that he wrote a gigantic test suite for all kinds of audio issues with several hundred tests. Yes, Vivaldi still fails some of them (but so do other browsers too, interestingly not all on the same tests) but it is still a work in progress, so stay tuned and watch the snapshot blog and the team blog 🙂


1) You can find it in the network tab of the developer tools. No need to try, it won’t give you a working file for streams.
2) Not really geek language, he isn’t like that – but understanding the correct technical terms is quite hard if you don’t have much expertise in an area. Seeing him pointing at the code blocks while he was explaining the detail was much easier (for me) than only getting a description of it.


Here the usual disclaimer:
This blog post reflects my personal opinion and views. For brevity (and dramatic reasons) I left out some of the boring parts, but everything else happened more or less as described here and as good as I could memorize what has happened. All errors and mistakes are mine and nothing in here is binding Vivaldi ASA or any person I mentioned in any way. It is only my personal, private view of the events I took part in as a “Soprano”. I have asked for permission to publish this (’cause NDA, ya know) to give a small look behind the scene because I thought it might be interesting for some of you, or maybe even entertaining, and I can only “Thank You!” for granting the request.

Why Vivaldi’s Reader (and others) sometimes don’t do what you expect

TL;DR

Vivaldi Read Mode was was never meant for pages like Twitter, Youtube comments, or Facebook chats but for articles

As general rule of thumb you can assume that everything that has a continuous or adjacent text chunk of more than 300 characters counts as content. It may be split in multiple adjacent paragraphs, but must look content-y enough (i.e. consist of sentences) and may not contain too many links, videos or images inside of the content area (outside is fine) and does not belong to one of the “stop” classes and IDs like e.g. “comment”, “footer”, “ad”-vertisement and many others.

/TL;DR

Still there?

History

The Vivaldi Read View is – like the read view in Mozilla Firefox and the Apple Safari – based on the Readability(TM) code that was released as a labs experiment by arc90 in about 2009 under an open source license (some versions under MIT, some under Apache license). Later arc90 changed it to a server supported version that is available at readability.com

The Intention Behind It

Readability was never meant to be an ad-blocker, but always as a on-demand reader view to switch on for *articles*, meaning: Longer passages of text (important!)

It was never intended to be used on pages like Facebook, with its gazillions of short text snippets, Youtube video comments, Twitter feeds and generally not on any page that does not contain a sizable longer chunk of text in one article.

It was meant to make reading of longer texts distraction free by removing e.g. advertisements, page navigation, comments and videos or images that don’t belong to the main article content, and to re-style it with readable fonts and colors to make reading more pleasurable. 

How?!

Of course the code is not really “intelligent” (it has to be fast and may not use up too many resources), so it has to trust on some kind of heuristics to detect where the main content might be. While generally it works quite well, it may fail on some pages, especially “if the HTML is totally hosed” (not my words, that was a comment of one of the original arc90 developers)

A (simplified and not complete) Explanation:

First steps:

  • Remove all scripts.
  • Remove all styles.
  • Ignore HTML block level elements like paragraphs and divisions with less than 25 characters completely.
  • Remove HTML block level elements that have “stop” classes or IDs or tags that indicate that they are definitely not content but something else like e.g navigation, footers, comments or advertisements etc.pp.

After that the reader loops through all paragraphs, and

  • calculates the over-all score for text length by the following formula: 
    rounded-down((pure-Text character count of a page element)/100)
    and adds it to the parent element (you might see it as a container). This means: A paragraph with less than 100 characters of text does not get any bonus at all.
  • adds a base score of 1 for each remaining paragraph to the parent element 
  • assigns a score to them based on how content-y they look. This score gets added to their parent node.
  • adds additional scores that are is determined by things like number of commas (+), class names and IDs (+/-), image and link density (More than 25% of the text is links? Too many images per paragraph? Punish it!) etc.
  • punishes List, Headline, Forms and Address and some other Elements with negative scores because they are normally not part of articles, and if they are, they are usually in the same parent container as the paragraphs in a real article, so the combined score of the parent element is still high enough to count.
  • adds half of the resulting score to the grandparent elements.

When that all is done and the parent or grandparent has a high enough score, it is seen as content and gets displayed, everything else gets removed.

Probably you can imagine now, how many pitfalls are there in which content detection may fall, so please take a break if you see it fail and think about what might have caused it this time.

Personal side note (strong language warning)

All in all content detection is a bіtch and can definitely fail on some pages, especially if the “Webmasters” (I call them Crapmasters) don’t know what a HTML validator is and have never heard about structured pages and accessibility. I am speaking out of experience: Back in 2009 I started with a userscript and later made an made an extension (cleanPages, see the old my.opera page on archive org) based the full original arc90 code and fine tuned it for Opera Presto (and ported it later for the new Opera thing). It had over 250k installs and while it was fun to tweak for better results, it was a hell of a lot of work. I wrote more than 200 versions with generic fixes for “just another couple of new pages that fail” but in the end I gave in and called it a day  – there are too many broken pages out there where the webmasters seemingly do not want people to read the content. Their wish is my command 😉

So please be gentle with the Vivaldi developers – yes, there is still some fine-tuning to be done, but that is really time consuming. It will probably have to wait because there are some other, more difficult and bigger things in the pipe (hint, hint 😀 )

Thank You!

Disclaimer: While I am a “Soprano” (aka external tester for internal builds), all the views in this text are my private views and do not necessarily reflect the views or opinions of Vivaldi (the company) or any of it’s owners or employees.

Followup on styling Vivaldi with CSS

Outdated. No longer needed! We have Themes now 🙂

Following up the post https://quhno.vivaldi.net/2015/07/02/some-quick-vivaldi-panels-css-hacks-for-better-readability-or-accessibility/

I’ve made some smallish changes that take care of some minor things I don’t like with the UI. They can be applied the same way as in the previous blog post.

This time I move the notes editor scrollbar to the right, make the speed dial navigation a little bit less high so that it aligns with the header of the web-panels, moved the speed dial items a bit upwards and colored the panel scrollbars for the dark UI.

Extrensions are great – Extensions inhibit progress

Sounds provocative?
Fine. It was planned to sound like that.

While we all go conform that it is impossible to cover every  need of a user with one software alone and that we therefore need a way to extend it, sometimes extensions can be a blocking stone for other development.
Continue reading “Extrensions are great – Extensions inhibit progress”

Restoring the Old Opera 12 Space Bar Behavior in Vivaldi

In the good old days of “The Real Opera(TM)” there was one feature for lazy people like me that I heavily used: The Space Bar!

I can hear you saying “The space bar is no feature, it is a key on the keyboard!” but that is only half of the truth:

If no edit field was focused but the page itself and you pressed the space bar, “The Real Opera(TM)” scrolled one page down. Nothing special so far, almost all browsers today do that (guess from which browser they copied that function ;)) but the other browsers missed one thing:

If you were on a paginated page or if the page had a LINK or an A rеl="neхt" element, or if simply the linktext contained “Next” or something similar in one of many languages,”The Real Opera(TM)” took you to the next page after you had scrolled to the bottom and hit the space bar again. I am one of those persons who actually read pages until the bottom and often follow the link to the next part of the article, so trying to restore this behavior was a must.

OK. Stop babbling and show us the code instead!

// ==UserScript== 
// @name Restore O12 space bar behavior
// @version 0.19 
// @author Roland "QuHno" Reck 
// @include http://*.* 
// @include https://*.* 
// ==/UserScript== 
(function (window) {
  "use strict";
  /*jshint browser: true, devel: false, debug: true, evil: true, forin: true, undef: true, bitwise: true, eqnull: true, noarg: true, noempty: true, eqeqeq: true, boss: true, loopfunc: true, laxbreak: true, strict: true, curly: true, nonew: true */
  function is_number(obj) {
    return !isNaN(obj - 0);
  }
  function make_ipattern(string) {
    return '/' + string + '/i';
  }
  function split_hash(obj) {
    //return (url + '#a').split('#')[0];
    return (obj.pathname + obj.search);
  }
  function getScrollMaxY() {
    return document.documentElement.scrollHeight - document.documentElement.clientHeight;
  }
  function scrollToPos(el, x, y) {
    el.scrollLeft = x;
    el.scrollTop = y;
  }
  var i,
  j,
  HREF = split_hash(window.location),
  PROTOCOL = window.location.protocol,
  HOST = window.location.host,
  HOSTPATTERN = make_ipattern(HOST),
  A,
  LRN,
  theParent,
  searchDepth,
  stop = false,
  linkContainer = false,
  regexps = {
    trim: /^\s+|\s+$/g,
    normalize: /\s{2,}/g,
    relLink: /^next$/gi,
    nextLink: /(Next\s*(page)?|Neste\s*(side)?|N(ä|ae)chste\s*(Seite)?|Weiter(e.*)?|Vorw(ä|ae)rts|Volg(ende)?\s*(bladsy|pagina)?|Verder|(Page)?\s*Suiv(ant)?(e)?(s)?|(Page)?\s*(prochaine)|Avanti|(Pag(ina)?)?\s*Succ(essiv(e|a|o)|Prossim(e|a|o))|Altr(a|o)|(P(á|a)gina)?\s*(S(e|i)guie?nte)|Próxim(a|os?)|Nästa\s*(sida)?|Næste?\s*(side)?|下一頁|下一页|Sonraki|Следующая|Далее| 下一页|下一张|下一篇|下一章|下一节|下一步|下一个|下页|后页|下一頁|下一張|下一節|下一個|下頁|後頁|다음|다음\s*페이지|次へ|Seuraava|Επόμενη|Следващ(а|о|и)\s*(страница|сайт)?|Нататък|След(в|н)а|(Пълен)?\s*Напред|Dalej|Następn(a|e|y)|Więcej|Tovább|Köv(etkező|\.)|Bővebben|Înaint(ar)?e(ază)?|Avansează|(Pagina\s*)?Următoa?r(e|ul)?|>([^\|]|$)|»([^\|]|$)|→)/i,
    extraneous: /print|archive|comment|discuss|e[\-]?mail|share|reply|all|login|sign|single|teaser/i,
    back: /(back|prev|earl|old|new|zurück|vorige|rückwärts|назад|<|«)/i,
    images: /\.jpe?g$|\.png$|\.webp$|\.gif$/i,
    cut: /[?#]/
  },
  actHREF,
  prevHREF,
  imageResult,
  shouldImagePreview = false,
  shouldFF = false,
  UUID = '43D82723-A99E-4BFD-ACDC-B7D8270EE75C';


  /* *********************************************************************************************
  FIND LINK TO NEXT PAGE
   */
  function analyze() {

    HREF = split_hash(window.location);
    PROTOCOL = window.location.protocol;
    HOST = window.location.host;
    HOSTPATTERN = make_ipattern(HOST);
    LRN = false;
    linkContainer = false;

    //* STEP 1: Look if the webmaster knew rel=  *//
    // If there is a <link rel="next" ... and it it points to the same origin: Use it.
    LRN = document.querySelector('link[rel="next"]');
    if (!!LRN && LRN.href.indexOf(window.location.origin) < 0) {
      LRN = false;
    }
    // If there is a <A rel="next" ... and it it points to the same origin: Use it.
    if (!LRN) {
      linkContainer = document.body.querySelector('A[rel="next"]');
      if (!!linkContainer && linkContainer.href.indexOf(window.location.origin) < 0) {
        linkContainer = false;
      }
    }

    if (!linkContainer) {

      A = document.body.querySelectorAll('A');
      for (i = 1, j = A.length - 2; i < j; [i++]) {
        // Skip this link if does not have the same origin
        if (A[i].href.indexOf(window.location.origin) < 0) {
          continue;
        }

        //* STEP 2: Try to find out if the classes and IDs of the parent elements give a hint on the next link. *//
        actHREF = split_hash(A[i]);
        prevHREF = split_hash(A[i - 1]);
        theParent = A[i];

        // We limit the search depth to 6 to avoid wading through too many elements
        searchDepth = 6;

        stop = false;
        while (searchDepth > 0 && stop === false && theParent.parentElement && theParent.parentElement !== document.body) {
          /*
          if (!theParent.parentElement) {
          // searchDepth = 0;
          break;
          }
           */

          theParent.classAndId = theParent.className + ' z ' + theParent.id;
          searchDepth = searchDepth - 1;

          // This is quite probably the wrong link.
          // May be this should be done outside of the loop because some navigations contain classes like "prev next" in the parent elements
          if (theParent.classAndId.match(regexps.back)) {
            stop = true;
            break;
          }

          // If we don't get "hard evidence" we don't want links in print or discussion things
          if (theParent.classAndId.match(regexps.extraneous)) {
            // stop = true;
            break;
          }

          // this could be the next link, if there is no better match later
          if (theParent.classAndId.match(regexps.nextLink)) {
            linkContainer = A[i];
          }
          theParent = theParent.parentElement;
        }
        //*
        // seems this hurts more than it is worth the time it saves
        if (stop) {
          continue;
        }
        // stop = false;
        //*/

        //* STEP 3: Look for numbered pagination like e.g. in forums *//
        if (is_number(A[i].textContent) && A[i].textContent > 0) {
          if (is_number(A[i - 1].textContent) === false || (A[i - 1].textContent - 0 === 0)) {
            if (is_number(A[i + 1].textContent) && A[i + 1].textContent > 0 && actHREF !== HREF && A[i].textContent !== '1') {
              linkContainer = A[i];
              continue;
            }
          }
          if (is_number(A[i - 1].textContent) && A[i - 1].textContent > 0) {

            // if the previous link is to the page where we are right now it is quite possible that this is the link to the next page
            if (prevHREF === HREF) {
              linkContainer = A[i];
              continue;

              // this is for skipped pagination where the actual page is not linked, so the link to the previous page is 2 lower than that to the next page.
            } else if ((A[i].textContent - A[i - 1].textContent - 0) === 2) {
              linkContainer = A[i];
              continue;
            }
          }
          // I don't know anymore why this was important, but it breaks somewhere if this is not in.
          if (prevHREF === HREF) {
            linkContainer = A[i];
            continue;
          }
        }

        //* STEP 4: Match IDs. Almost as good as rel, therefore: If there is a good ID, we take it and stop searching *//
        if (A[i].id) {
          if (A[i].id.match(regexps.nextLink)) {
            linkContainer = A[i];
            break;
          }
        }

        //* STEP 5: CLASSes are weaker than ID, so store but don't stop. If there is no better Link we take it. *//
        if (A[i].className) {
          if (A[i].className.match(regexps.nextLink)) {
            linkContainer = A[i];
            continue;
          }
        }

        //* STEP 6: We are down to textContent - quite weak, but better than nothing *//
        if (A[i].textContent) {
          if (A[i].textContent.replace(/\s+/g, ' ').length < 25 && A[i].textContent.match(regexps.nextLink)) {
            linkContainer = A[i];
            continue;
          }
        }

        //* STEP 7: TITLE, even weaker ... *//
        if (A[i].getAttribute("title")) {
          if (A[i].textContent.replace(/\s+/g, ' ').length < 25 && A[i].getAttribute("title").match(regexps.nextLink)) {
            linkContainer = A[i];
            continue;
          }
        }

      }

    }

    // Comment this out if you don't want a hint about the next link
    // set borders _and_ outlines because in most cases websites don't alter both at the same time
    if (!!linkContainer) {
      linkContainer.style.border = '1px solid #FF0000';
      linkContainer.style.outline = '1px solid #FFff00';
      if (!document.querySelector('a[rel="next"]')) {
        linkContainer.setAttribute('rel', 'next');
        // Experimental: Maybe the Fast Forward button reacts on that at some time in the future ... probably not.
        var evt = document.createEvent('Event');
        evt.initEvent('load', false, false);
        window.dispatchEvent(evt);
      }
    }
  }
  function doFastForward() {
    /* Hard coded exceptions for fucked up search engine pages */
    var temp = null;
    /* extra saussage for startpage.com */
    if (HOST.indexOf('startpage.com') > -1) {
      temp = document.querySelector('#nextnavbar > form > a > span.i_next');
      console.log('startpage.com fucked up');
      temp.click();
      return false;
    }

    /* Normal stuff */
    /* we've got a hard link rel */
    if (LRN && LRN.href) {
      window.location.href = LRN.href;
      return false;
    }
    if (!!linkContainer) {
      linkContainer.click();
    }
  }

  var handleKeyDown = function (e) {
    if (e.key === "PageDown" && shouldFF) {
      doFastForward();
    }
    // Don't space2next if the focus is in an editable area
    // Did I forget something?
    if (/INPUT|SELECT|TEXTAREA|CANVAS/.test(e.target.tagName) || e.target.isContentEditable || document.designMode === 'on') {
      return;
    }

    // ToDo: If there are linked imageExtensions, add the logic to show them

    // If the page is already scrolled to the bottom and the user hits space, go to the next page if a link was found.
    if (e.keyCode === 32 && shouldFF) {
      doFastForward();
    }
  };

  window.addEventListener('scroll', function (e) {

    // don't analyze if the user did not scroll to the bottom
    if (window.scrollY >= getScrollMaxY() && window.scrollY > 0) {
      analyze();
      shouldFF = true;
    } else if (window.scrollY < getScrollMaxY()) {
      console.log(shouldFF);
      shouldFF = false;
    }
  }, false);
  window.addEventListener('keydown', handleKeyDown, false);
})(window);

 

  • Copy the code into a decent text editor of your choice (Please make sure your editor is set to UTF-8 or you will get problems)
  • Save it as restoreSpaceBar.user.js (yes, with 2 dots.) to some place where you can find it again
  • Open vivaldi://extensions
  • Drag and drop that file on (in?) the page (you might need to enable the developer mode on the extensions management page before)
  • Confirm that you want to install it. That’s it.

 

Warning: It is still a work in progress and may break things.

If you have any idea how to improve it:

Feel free to leave a comment 🙂

PS: I totally forgot to put some tracking code into the monster RegExp 😀

Some quick Vivaldi Javascript hacks to add additional functions

Warning: Do not apply this code to a current version of Vivaldi. It will not work anymore because the Vivaldi code has changed!

 

As follow-up to the previous entry about Hacking the CSS, this time we add some new functions to Vivaldi. The Idea is based on the posts you can find at http://habrahabr.ru/users/23rd/topics/ (in Russian, Google translate helps if you don’t speak Russian). I changed them a bit to avoid some errors I saw in the debugging console and to make sure that they only do what they should (there were some minor glitches where unexpected things happened if you clicked in the wrong place – especially if you run the browser in UI debugging mode)

I ported 2 of the scripts:

“Click to minimize/hide the tab” which deactivates the current tab and shows the last active tab. (don’t click at the [x] – that still closes the tab ;))

“Right-click [+] button to paste and go in a new tab.

 

For testing reasons you can deactivate each of them by removing the first slash at the leading //*.

How to …?

Look in the application folder of Vivaldi for browser.html in the ressources/vivaldi folder and open it with a text editor.

Insert after the string and save it.

Create a folder in the same directory as browser.html and rename the folder to custom_ui . It should be already in the right place if you followed Method 2 in the previous entry about Hacking the CSS.

Save the following code as custom.js into the new folder:

 

(function () {     // get the whole browser UI. Do this only once!     var browser = document.body.querySelector('#browser');          // we need to emulate a click sometimes. Do this only once!     var dispatchMouseEvent = function (target, var_args) {         var e = document.createEvent("MouseEvents");         e.initEvent.apply(e, Array.prototype.slice.call(arguments, 1));         target.dispatchEvent(e);     };      /*     Single click on a tab to hide it and show the last active tab.      */ //*     var list = [];     function listTabs() {         var tabs = browser.querySelectorAll('#tabs>.tab');         list = [];         for (var i = 0; i < tabs.length; i++) {             list.push(tabs[i]);         }         list.sort(function (a, b) {             return recent(b) - recent(a);         });     }          function recent(tab) {         var page = document.querySelector('.webpageview webview[tab_id="' + tab.dataset.tabId + '"]');         if (page) {             page = page.parentNode.parentNode.parentNode.parentNode;             return parseInt(page.style.zIndex);         }         return 0;     };      function startAction(e) {         for (var i = 0; i < e.path.length; i++) {             // clicking on tab group switches should not fire, neither should clicking anywhere in the webpageview             if (e.path[i].classList && (e.path[i].classList.contains('tab-indicator') || e.path[i].classList.contains('webpageview'))) {                 break;             }             if (e.path[i].classList && e.path[i].classList.contains('active')) {                 var active = browser.querySelector('.tab.active');                 listTabs();                 dispatchMouseEvent(list[1], 'mousedown', true, true);                 break;             }         }     };          browser.addEventListener('mousedown', function (e) {         // Make sure that it fires on left mouse click only         //  e = e || window.event;         switch (e.which) {         case 1:             startAction(e);             break; // left         case 2:             break; // middle         case 3:             break; // right         }     }); //*/       /*     Right click on plus-button to paste and go      */ //*     var isItMouse = false; // Exclude responses from keyboard      //Tweak for paste in this input-field     var hiddenInput = document.createElement("input");     hiddenInput.type = "text";     browser.appendChild(hiddenInput);     hiddenInput.style.width = "0px";     hiddenInput.style.height = "0px";     hiddenInput.style.display = "none";      browser.addEventListener('contextmenu', function (e) {         // Area near square         if (e.target.className.toString().indexOf('newtab') > -1) {             isItMouse = true;             document.execCommand('paste');             return;         }         // Plus-symbol         // changed to parentElement instead of parentNode         if (e.target.parentElement.className.indexOf('newtab') > -1) {             initPaste();             return;         }         // Square         if (e.target.parentElement.parentElement.className.indexOf('newtab') > -1) {             initPaste();             return;         }     });      function initPaste() {         isItMouse = true;         hiddenInput.style.display = "block";         hiddenInput.focus();         document.execCommand('paste');     }      document.addEventListener('paste', function (e) {         if (isItMouse) {             isItMouse = false;             var url = e.clipboardData.getData('text/plain');             hiddenInput.style.display = "none"; //hide input-field for pasting              var re = new RegExp('\\r\\n', 'g'); // Delete newline characters             url = url.replace(re, '');                          // Search engines             var searchEngine = 'https://google.com/webhp?q=';             // var searchEngine = 'http://yandex.ru/search/?text=';             // var searchEngine = 'https://duckduckgo.com/?q=';             // ... or insert search string of your favorite search engine                          var active = browser.querySelector('.tab.active');             var webview = document.querySelector('#webview-container webview[tab_id="' + active.dataset.tabId + '"]');              if (url.length > 0) {                 if (checkUrl(url)) {                     webview.executeScript({                         code : "window.open('" + url + "','_blank')"                     });                 } else if (checkUrlWithoutProtocol(url)) {                     webview.executeScript({                         code : "window.open('http://" + url + "','_blank')"                     });                 } else {                     webview.executeScript({                         code : "window.open('" + searchEngine + url + "','_blank')"                     });                 }             }             //    console.log(url)         }     });          //Check url     var patternUrl = /^(?:(?:https?|ftp):\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?/#]\S*)?$/i;     var patternUrlWithout = /^(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,3})).?)(?::\d{2,5})?(?:[/?/#]\S*)?$/i;          //url with protocol     function checkUrl(str) {         return patternUrl.test(str);     }          //url without protocol     function checkUrlWithoutProtocol(str) {         return patternUrlWithout.test(str);     } //*/ })();

Some quick Vivaldi panels CSS hacks for better readability or accessibility

Deprecated. While the basic technique would still work, too much has changed in the new Versions of Vivaldi and now we have Themes.

While I generally like the design idea that stands behind behind Vivaldi, I have some problems with it. I don’t know which screens the designers use or if they are calibrated, if their eyesight is perfect or not, but the contrast of some elements in the default light and dark themes is a little bit too low for me to feel comfortable, especially in the panels. Luckily the whole UI of Vivaldi is based on web technology—or in non marketing speak: HTML, JavaScript and CSS—it is quite easy to hack.

b2ap3_thumbnail_Vivaldi-Panel-CSS-Mod_20150702-163235_1.png

Yes I resized the screenshot deliberately to a smaller size. As you can see everything in the panels stays visible, despite the smaller size . You can try that with the default interface, but don’t use sharpening while resizing 😉

The following hack does not solve all problems I see, e.g. the less than sufficient contrast of the Tab titles in the dark interface if you do not use transparent tabs, but you can see it as a first step for your own experiments.

 

How to …?

Method 1:

Look for common.css in the application folder of the browser and open it with a text editor.

Add the following to the end of it (make sure you add it at the end!)

Method 2:

Look in the application folder of Vivaldi for browser.html in the ressources/vivaldi folder and open it with a text editor.

Insert <link rel="stylesheet" href="custom_ui/panel.css" /> after the string <link rel="stylesheet" href="style/common.css" /> and save it.

Create a folder in the same directory as browser.html and rename the folder to custom_ui

Save the following code as panel.css into the new folder:

 

#switch {     background: #dfdfdf;     box-shadow: 2px 0 0 -1px rgba(255, 255, 255, .3), -2px 0 0 -1px rgba(255, 255, 255, .3); } .ui-dark #switch {     background: #333333;     box-shadow: 2px 0 0 -1px rgba(255, 255, 255, .9), -2px 0 0 -1px rgba(255, 255, 255, .9); } .ui-light #switch button svg {     fill: rgba(0, 0, 0, .7) } .ui-dark #switch button svg {     fill: rgba(255, 255, 255, .7) } .ui-dark #panel_switch {     background-color: #3f3f3f;     box-shadow: 2px 0 0 -1px rgba(0, 0, 0, .4), -2px 0 0 -1px rgba(0, 0, 0, .4); } .ui-light .panel-group {     box-shadow: 2px 0 0 -1px rgba(0, 0, 0, .2), -2px 0 0 -1px rgba(0, 0, 0, .2); } .ui-dark .panel-group {     box-shadow: 2px 0 0 -1px rgba(255, 255, 255, .2), -2px 0 0 -1px rgba(255, 255, 255, .2); } .ui-light #switch button.active {     box-shadow: inset 1px 1px 1px 0px rgba(0, 0, 0, .3); } .ui-dark #switch button.active {     background-color: #464646;     box-shadow: inset 1px 1px 1px 0px rgba(255, 255, 255, .3); } .ui-light #switch button:not(.active):hover {     background: #eeeeee; } .ui-dark #panels-container.left #panel_switch,  #panels-container.right #panel_switch.off {     background-repeat: no-repeat;     background-position: center;     background-size: contain;     /* arrow-left white */     background-image: url("data:image/svg+xml;utf8,"); } .ui-dark #panels-container.left #panel_switch.off, #panels-container.right #panel_switch {     background-repeat: no-repeat;     background-position: center;     background-size: contain;     /* arrow-right white */     background-image: url("data:image/svg+xml;utf8,"); } .ui-dark .addbookmark-cardwrapper, .ui-dark .downloaddialog-cardwrapper, .ui-dark .notes-cardwrapper {     box-shadow: inset 1px 1px 0 0px rgba(255, 255, 255, .2), 2px 2px 0px 1px rgba(0, 0, 0, .4); } /* Dark UI: Make the color of the font a bit lighter in the tab .ui-dark #tabs .tab{color:rgba(255,255,255,.6);  */  .ui-dark #tabs .tab {     color: rgba(255, 255, 255, .8);     background-color: #333; } .ui-dark #tabs .tab:hover {     color: rgba(255, 255, 255, 1);     box-shadow: inset 1px 1px 1px 0px rgba(255, 255, 255, .8); } .ui-dark #tabs .tab:not(.active):hover {     background-color: #444; }  .ui-dark #tabs .tab.unread.tab-transparent {     box-shadow: inset 0 -2px 0 0 #CCCCCC,inset 0 -4px 0 0 rgba(0,0,0,.2); }  .ui-dark  .close {     background-image: url(/resources/close-white.png); }  .ui-dark  #footer .paneltogglefooter svg {     fill: rgba(255, 255, 255, .9) }  .ui-dark #footer #status_info {     color: #ffffff; } /* Make the free space right to the tabs draggable if tab container is at top #browser:not(.mac) .tab-spacer,#tabs .tab,#zoom_control,.tab-group-indicator,.topmenu,button,input{-webkit-app-region:no-drag}  Only needed for builds lower than 1.0.230.#, see help > about */ /*  #browser:not(.mac) .top .tab-spacer{ -webkit-app-region:drag } */  .vivaldi-tree .tree-row.folder .folder-item-count {     opacity: .5;     color: rgba(0, 0, 0, 1) } .ui-dark .vivaldi-tree .tree-row.folder .folder-item-count {     opacity: .5;     color: rgba(255, 255, 255, 1) } .hasfocus .bookmark-bar button:hover img.icon {     background-color: rgba(255, 255, 255, 0);     border-color: rgba(255, 255, 255, 0); } .bookmark-bar button img.icon {     background-color: rgba(255, 255, 255, 0);     border-color: rgba(255, 255, 255, 0); } .bookmark-bar button {     border: 1px outset rgba(0, 0, 0, 0) } .hasfocus .bookmark-bar button:hover {     -webkit-filter: saturate(150%) brightness(150%); } /* a bit of blue tint ... */  .vivaldi-tree .tree-row:not([data-selected]):hover {     background-color: rgba(0, 110, 215, .4); } .ui-dark .vivaldi-tree .tree-row:not([data-selected]):hover {     background-color: rgba(0, 110, 215, .4); } .vivaldi-tree .tree-row[data-selected] , .vivaldi-tree .tree-row[data-selected]:hover{     color: #eee;     background-color: #006ed7;     border: 0 } .vivaldi-tree .tree-row[data-selected]:hover{     color: #fff; } .vivaldi-tree .tree-row[data-selected][data-nofocus] {     background-color: rgba(54, 95, 135, .2);     color: inherit;     border: 0 } .ui-dark .vivaldi-tree .tree-row[data-selected][data-nofocus] {     background-color: rgba(54, 95, 135, 1);     color: inherit;     border: 0 }  /* a bit of blue tint ... */  .ui-dark ::-webkit-scrollbar {      width: 15px;      height: 15px; } .ui-dark ::-webkit-scrollbar-button {       background-color: #333;      border: 1px solid #555; } .ui-dark ::-webkit-scrollbar-track {       background-color: #000000; } .ui-dark ::-webkit-scrollbar-track-piece {      background-color: #333333; } .ui-dark ::-webkit-scrollbar-thumb {      height: 50px;      background-color: #666; } .ui-dark ::-webkit-scrollbar-corner {      background-color: #999; } .ui-dark ::-webkit-resizer {      background-color: #666; } .ui-dark ::-webkit-scrollbar-button {     background-repeat:no-repeat;     background-position:center;     background-size: contain; } .ui-dark ::-webkit-scrollbar-button:vertical:increment {     /* arrow-down white */     background-image: url("data:image/svg+xml;utf8,"); } .ui-dark ::-webkit-scrollbar-button:vertical:decrement {     /* arrow-up white */     background-image: url("data:image/svg+xml;utf8,"); } .ui-dark ::-webkit-scrollbar-button:horizontal:increment {     /* arrow-right white */     background-image: url("data:image/svg+xml;utf8,"); } .ui-dark ::-webkit-scrollbar-button:horizontal:decrement {     /* arrow-left white */     background-image: url("data:image/svg+xml;utf8,"); }

 

edit: There is a follow up about Hacking the UI with Javascript to add functions.

PO2JSON converter for Vivaldi translation purposes

Outdated. Use André’s online converter instead.

You can find it here: https://an-dz.github.io/PurePO2JSON/


Translating Vivaldi was always a bit difficult when it came to string lengths or if we needed to see exactly where the string is and if it is the right one in that place. Luckily An_dz invested some time to write a nice little JavaScript for the dev console that can convert files from the POEDIT format used for translation to the JSON format used in Vivaldi, which works well …

… but I am a lazy guy and did not want to copy the file to the console each time I needed to test new translations, so I modified some (few) lines in his script, added some file read/write stuff and stuffed it into a simple webpage. When run locally, it allows to load the .po file,  convert and save it as messages.json file. This file can be used with Vivaldi to test the translations.

 

Alternatively you can use ludev‘s online converter which is based on this script. It can either download the PO file directly from Transifex, convert it for you and store it to your computer for further use – or you can use your local PO file for the conversion.

The online converter is availlable at http://ludev.rocks/vivaldipo2json/.

 

The code:


<!DOCTYPE html> <html>      <head>         <meta http-equiv="Content-Type" content="text/html; charset=utf-8">         <title>Convert Vivaldi PO to JSON</title>         <style>             body {                 font-family: sans-serif;             }             textarea {                 width: 90%;                 height: 600px;             }          </style>     </head>      <body>         <form>             <div>                 <p id="status">File read</p>                 <textarea id="output"></textarea>             </div>             <input type="file" id="files" name="files">         </form>         <script>             function convertToJson(file) {                 'use strict';                 // VivaldiPO2JSON v1.1                 // by André Zanghelini (An_dz)                 /* modified by Roland Reck (QuHno) to work with the web page,  				* added some missing "," and "=" 				* added some {}, replaced i++ by i += 1 and moved some var declarations to other places because JSLint was complaining 				* 2016-01-25: changed order of functions so that they are already defined _before_ they are caled. 				* 2016-01-25: replace multiple space by one 				*/                  var textArea = document.getElementById('output'),                     start = 0,                     end = 0,                     i = 0,                     oldStr = '',                     newStr = '',                     s,                     tempStr = '', 					arr = [], 					msgid;                  // Remove useless header stuff                 file = file.replace(/msgid ""\n/, "");                 file = "{" + file.substring(file.indexOf("msgid"));                 // Remove other comments                 file = file.replace(/(#.*\n|"\n")/g, "");                 // Change translation strings to JSON message                 file = file.replace(/msgstr (".*")\n/g, "    {\"message\":$1},");                 // Change msgid so Vivaldi can understand them                 // Special and uppercase chars must be decimal unicode value                 tempStr = file;                 // While we still find "msgid "                 while (start > -1) {                     start = tempStr.search(/msgid /);                     if (start > -1) {                         end = start + tempStr.substr(start).search(/\n/);                         oldStr = tempStr.substring(start, end);                         newStr = oldStr.replace(/msgid /, "");                         s = 0; 						// Q-mod: Replace multiple whitespace by one 						newStr = newStr.replace(/\s+/g, " ");                         // We change the special characters for later manipulation                         newStr = newStr.replace(/([A-Z(){}å\[\]—.,;:\/\\<>\=\+*#@'\|!’%&$“”?\_\-]| )/g, "%$1_");                         // Now we get rid of the newlines which are written as \n                         // Remember that the above added a % in front of \n and an _ after \                         newStr = newStr.replace(/%\\_n/g, "_10_");                         // Add 0 at end of string name and : at end of string                         newStr = newStr.replace(/(.)"/, "$10\":");                         // Replace special chars and uppercase to char code                         while (s !== -1) {                             s = newStr.search(/%/);                             if (s > -1) {                                 newStr = newStr.replace(/%./, "_" + newStr.charCodeAt(s + 1)); 							}                         }                          file = file.replace(oldStr, newStr);                         // Just search where we did not search before                         tempStr = tempStr.substr(end + 1);                     }                 }                  // Replace plurals, start copying msgid                 arr = file.split("\nmsgid_plural");                 file = arr[0];                 for (i = 1; i < arr.length; i += 1) {                     // Take msgid from previous part                     msgid = arr[i - 1].substring(arr[i - 1].lastIndexOf("\n\"") + 1, arr[i - 1].lastIndexOf(":") - 2);                     // remove msgid_plural and replace msgstr[0] in current part                     arr[i] = arr[i].replace(/.*\nmsgstr\[0\] (".*")/, "\n    {\"message\":$1},");                     // Replace other variations                     arr[i] = arr[i].replace(/msgstr\[(\d+)\] (".*")/g, msgid + "$1\":\n    {\"message\":$2},");                     // Join again to main                     file = file.concat(arr[i]);                 }                  // Add msgctxt string                 arr = file.split("msgctxt ");                 file = arr[0];                 for (i = 1; i < arr.length; i += 1) {                     // Add # at begining, this will help our selector                     arr[i] = "#" + arr[i];                     // Find first newline                     end = arr[i].search(/\n/);                     // Find spaces, special chars and uppercase in first line                     newStr = arr[i].substring(1, end - 1).replace(/([A-Z(){}\[\].,;:\/\\<>\=\+*#@'\|!%&$“”?\_\-]| )/g, "%$1_");                     // Replace special chars and uppercase to char code                     s = 0;                     while (s !== -1) {                         s = newStr.search(/%/);                         if (s > -1) {                             newStr = newStr.replace(/%./, "_" + newStr.charCodeAt(s + 1)); 						}                     }                     arr[i] = arr[i].replace(/#.*\n"/, newStr + "_4_");                     // arr[i] = arr[i].replace(/"\n"/, "_4_")                     file = file.concat(arr[i]);                 }                  // Remove double newline                 file = file.replace(/\n\n/g, "\n");                  // Replace last comma to closing bracket                 file = file.substring(0, file.lastIndexOf(",")) + "\n}";                  // Replace on screen and alert                 textArea.textContent = file;                 document.getElementById('status').innerText = "File successfully converted";              }               function saveTextAsFile(fileNameToSaveAs) { 			    'use strict';                 var textToWrite = document.getElementById("output").value,                     textFileAsBlob = new Blob([textToWrite], {                         type : 'application/json'                     }),                     downloadLink = document.createElement("a"); // Create temporary link. No need to add it to the DOM                  downloadLink.download = fileNameToSaveAs;                 downloadLink.innerText = "Download File";                 downloadLink.href = window.URL.createObjectURL(textFileAsBlob);                 downloadLink.click();                 // Do I need to remove the objectURL? 				window.URL.revokeObjectURL(downloadLink.href);             }  			function handleFileSelect(evt) { 			    'use strict';                 var files = document.getElementById('files').files,                     reader = new FileReader();                  if (!files.length) {                     alert('Please select a file!');                     return;                 }                  //var file = files[0];                 // Not the best check but should be sufficient                 if (files[0].name.search(/\.po$/i) === -1) {                     alert('Please select a .po file!');                     files = null;                     return;                 }                   reader.onloadend = function (evt) {                     if (evt.target.readyState === FileReader.DONE) { // DONE == 2                         convertToJson(evt.target.result);                         // Extract file name from e.g. "for_translation_vivaldi-localisation_vivaldipot_de.po" to "de.json" for saving                         var start = files[0].name.search(/vivaldipot/i),                             end = files[0].name.search(/\.po$/i),                             filename = files[0].name.substring(start + 11, end) + '.json';                         saveTextAsFile(filename);                     }                 };                 reader.readAsText(files[0], "UTF-8");             }               document.getElementById('files').addEventListener('change', handleFileSelect, false);         </script>     </body>  </html>

 

Usage:

  • Save the code as VivaldiPO2JSON.html to your computer somewhere where you can find it again
  • Open it in Vivaldi
  • Choose the right PO file
  • Convert it
  • Save the converted file

All mistakes are mine An_dz code works just fine. Feel free to post improvements below 🙂

 

edit: Thanks to An_dz for the changes! (See his comment below) 🙂

Thanks to ludev for providing the online converter.