Prechádzať zdrojové kódy

Add options UI for each origin individually +
Add CSP option for each origin individually +
Remove default UTF-8 encoding +
Add Encoding option for each origin individually

simov 7 rokov pred
rodič
commit
9a39938561
9 zmenil súbory, kde vykonal 400 pridanie a 178 odobranie
  1. 22 23
      background/detect.js
  2. 12 15
      background/headers.js
  3. 13 7
      background/messages.js
  4. 19 2
      background/storage.js
  5. 8 22
      content/content.js
  6. 193 73
      content/options.js
  7. 13 11
      css/icons.css
  8. BIN
      css/icons.ttf
  9. 120 25
      css/options.css

+ 22 - 23
background/detect.js

@@ -33,8 +33,8 @@ md.detect = ({storage: {state}, inject}) => {
           return
         }
 
-        if (match(win.header, win.url)) {
-          if (onwakeup && state.csp) {
+        if (header(win.header) || match(win.url)) {
+          if (onwakeup && state.intercept) {
             onwakeup = false
             chrome.tabs.reload(id)
           }
@@ -46,31 +46,30 @@ md.detect = ({storage: {state}, inject}) => {
     }
   }
 
-  var match = (header, url) => {
-    if (state.header && header && /text\/(?:x-)?markdown/i.test(header)) {
-      return true
-    }
-    else {
-      var location = new URL(url)
+  var header = (value) => {
+    return state.header && value && /text\/(?:x-)?markdown/i.test(value)
+  }
 
-      var path =
-        state.origins[location.origin] ||
-        state.origins['*://' + location.host] ||
-        state.origins['*://*']
+  var match = (url) => {
+    var location = new URL(url)
 
-      // ff: webRequest bug - does not match on `hostname:port`
-      if (!path && /Firefox/.test(navigator.userAgent)) {
-        var path =
-          state.origins[location.protocol + '//' + location.hostname] ||
-          state.origins['*://' + location.hostname] ||
-          state.origins['*://*']
-      }
+    var origin =
+      state.origins[location.origin] ||
+      state.origins['*://' + location.host] ||
+      state.origins['*://*']
+
+    // ff: webRequest bug - does not match on `hostname:port`
+    if (!origin && /Firefox/.test(navigator.userAgent)) {
+      var origin =
+        state.origins[location.protocol + '//' + location.hostname] ||
+        state.origins['*://' + location.hostname] ||
+        state.origins['*://*']
+    }
 
-      if (path && new RegExp(path).test(location.href)) {
-        return true
-      }
+    if (origin && new RegExp(origin.match).test(location.href)) {
+      return origin
     }
   }
 
-  return {tab, match}
+  return {tab, header, match}
 }

+ 12 - 15
background/headers.js

@@ -6,29 +6,26 @@ md.headers = ({storage: {state}, detect}) => {
       return {responseHeaders}
     }
 
-    var header = responseHeaders.find(({name}) => /content-type/i.test(name))
+    var header = responseHeaders.find(({name}) => /content-type/i.test(name)) || {}
+    var origin = detect.match(url)
 
-    if (!detect.match(header, url)) {
+    if (!detect.header(header.value) && !origin) {
       return {responseHeaders}
     }
 
-    if (state.csp) {
+    if (origin.csp) {
       responseHeaders = responseHeaders
         .filter(({name}) => !/content-security-policy/i.test(name))
     }
 
-    if (/Firefox/.test(navigator.userAgent)) {
-      responseHeaders = responseHeaders
-        // ff: markdown `content-type` is not allowed
-        .map((header) => {
-          if (
-            /content-type/i.test(header.name) &&
-            /text\/(?:x-)?markdown/.test(header.value)
-          ) {
-            header.value = 'text/plain; charset=utf-8'
-          }
-          return header
-        })
+    // ff: markdown `content-type` is not allowed
+    if (/Firefox/.test(navigator.userAgent) && detect.header(header.value)) {
+      header.value = 'text/plain'
+    }
+
+    if (origin.encoding && header.name) {
+      var [media] = header.value.split(';')
+      header.value = `${media}; charset=${origin.encoding}`
     }
 
     return {responseHeaders}

+ 13 - 7
background/messages.js

@@ -78,26 +78,32 @@ md.messages = ({storage: {defaults, state, set}, compilers, mathjax, headers}) =
       sendResponse({
         origins: state.origins,
         header: state.header,
-        csp: state.csp,
-        exclude: state.exclude,
+        intercept: state.intercept,
+        match: state.match,
       })
     }
     else if (req.message === 'options.header') {
       set({header: req.header})
       sendResponse()
     }
-    else if (req.message === 'options.csp') {
+    else if (req.message === 'options.intercept') {
       // ff: onHeadersReceived is enabled by default
       if (!/Firefox/.test(navigator.userAgent)) {
-        headers[req.csp ? 'add' : 'remove']()
+        if (req.intercept !== state.intercept) {
+          headers[req.intercept ? 'add' : 'remove']()
+        }
       }
-      set({csp: req.csp})
+      set({intercept: req.intercept})
       sendResponse()
     }
 
     // options origins
     else if (req.message === 'origin.add') {
-      state.origins[req.origin] = defaults.match
+      state.origins[req.origin] = {
+        match: defaults.match,
+        csp: false,
+        encoding: '',
+      }
       set({origins: state.origins})
       sendResponse()
     }
@@ -107,7 +113,7 @@ md.messages = ({storage: {defaults, state, set}, compilers, mathjax, headers}) =
       sendResponse()
     }
     else if (req.message === 'origin.update') {
-      state.origins[req.origin] = req.match
+      state.origins[req.origin] = req.options
       set({origins: state.origins})
       sendResponse()
     }

+ 19 - 2
background/storage.js

@@ -16,10 +16,14 @@ md.storage = ({compilers}) => {
     },
     raw: false,
     header: true,
-    csp: false,
+    intercept: false,
     match,
     origins: {
-      'file://': match
+      'file://': {
+        match,
+        csp: false,
+        encoding: '',
+      }
     },
   }
 
@@ -83,6 +87,19 @@ md.storage = ({compilers}) => {
     if (options.csp === undefined) {
       options.csp = false
     }
+    // v3.4 -> v3.5
+    if (typeof options.origins['file://'] === 'string') {
+      options.origins = Object.keys(options.origins)
+        .reduce((all, key) => (all[key] = {
+          match: options.origins[key],
+          csp: options.csp,
+          encoding: '',
+      }, all), {})
+    }
+    if (typeof options.csp === 'boolean') {
+      options.intercept = options.csp
+      delete options.csp
+    }
 
     // reload extension bug
     chrome.permissions.getAll((permissions) => {

+ 8 - 22
content/content.js

@@ -49,28 +49,14 @@ function mount () {
 
   m.mount($('body'), {
     oninit: () => {
-      ;((done) => {
-        if (document.charset === 'UTF-8') {
-          done()
-          return
-        }
-        m.request({method: 'GET', url: location.href,
-          deserialize: (body) => {
-            done(body)
-            return body
-          }
-        })
-      })((data) => {
-        state.markdown = data || md
-
-        chrome.runtime.sendMessage({
-          message: 'markdown',
-          compiler: state.compiler,
-          markdown: state.markdown
-        }, (res) => {
-          state.html = state.content.emoji ? emojinator(res.html) : res.html
-          m.redraw()
-        })
+      state.markdown = md
+      chrome.runtime.sendMessage({
+        message: 'markdown',
+        compiler: state.compiler,
+        markdown: state.markdown
+      }, (res) => {
+        state.html = state.content.emoji ? emojinator(res.html) : res.html
+        m.redraw()
       })
     },
     view: () => {

+ 193 - 73
content/options.js

@@ -3,7 +3,7 @@ var defaults = {
   // storage
   origins: {},
   header: false,
-  csp: false,
+  intercept: false,
   // static
   protocols: ['https', 'http', '*'],
   // UI
@@ -11,6 +11,27 @@ var defaults = {
   origin: '',
   timeout: null,
   file: true,
+  encodings: {
+    'Unicode': ['UTF-8', 'UTF-16LE'],
+    'Arabic': ['ISO-8859-6', 'Windows-1256'],
+    'Baltic': ['ISO-8859-4', 'ISO-8859-13', 'Windows-1257'],
+    'Celtic': ['ISO-8859-14'],
+    'Central European': ['ISO-8859-2', 'Windows-1250'],
+    'Chinese Simplified': ['GB18030', 'GBK'],
+    'Chinese Traditional': ['BIG5'],
+    'Cyrillic': ['ISO-8859-5', 'IBM866', 'KOI8-R', 'KOI8-U', 'Windows-1251'],
+    'Greek': ['ISO-8859-7', 'Windows-1253'],
+    'Hebrew': ['Windows-1255', 'ISO-8859-8', 'ISO-8859-8-I'],
+    'Japanese': ['EUC-JP', 'ISO-2022-JP', 'Shift_JIS'],
+    'Korean': ['EUC-KR'],
+    'Nordic': ['ISO-8859-10'],
+    'Romanian': ['ISO-8859-16'],
+    'South European': ['ISO-8859-3'],
+    'Thai': ['Windows-874'],
+    'Turkish': ['Windows-1254'],
+    'Vietnamese': ['Windows-1258'],
+    'Western': ['ISO-8859-15', 'Windows-1252', 'Macintosh'],
+  }
 }
 var state = Object.assign({}, defaults)
 
@@ -27,28 +48,6 @@ var events = {
     })
   },
 
-  csp: (e) => {
-    ;((done) => {
-      // ff: webRequest is required permission
-      if (/Firefox/.test(navigator.userAgent)) {
-        done()
-      }
-      else {
-        var action = state.csp ? 'remove' : 'request'
-        chrome.permissions[action]({
-          permissions: ['webRequest', 'webRequestBlocking']
-        }, done)
-      }
-    })(() => {
-      state.csp = !state.csp
-      chrome.runtime.sendMessage({
-        message: 'options.csp',
-        csp: state.csp,
-      })
-      m.redraw()
-    })
-  },
-
   origin: {
     protocol: (e) => {
       state.protocol = state.protocols[e.target.selectedIndex]
@@ -88,22 +87,82 @@ var events = {
       })
     },
 
-    update: (origin) => (e) => {
-      state.origins[origin] = e.target.value
+    refresh: (origin) => () => {
+      chrome.permissions.request({origins: [origin + '/*']})
+    },
+
+    match: (origin) => (e) => {
+      state.origins[origin].match = e.target.value
       clearTimeout(state.timeout)
       state.timeout = setTimeout(() => {
+        var {match, csp, encoding} = state.origins[origin]
         chrome.runtime.sendMessage({
-          message: 'origin.update', origin, match: e.target.value
+          message: 'origin.update',
+          origin,
+          options: {match, csp, encoding},
         })
       }, 750)
     },
 
-    refresh: (origin) => () => {
-      chrome.permissions.request({origins: [origin + '/*']})
+    csp: (origin) => () => {
+      state.origins[origin].csp = !state.origins[origin].csp
+      var {match, csp, encoding} = state.origins[origin]
+      chrome.runtime.sendMessage({
+        message: 'origin.update',
+        origin,
+        options: {match, csp, encoding},
+      })
+      webRequest.update()
+      webRequest.permission(() => {
+        webRequest.register()
+      })
+    },
+
+    encoding: (origin) => (e) => {
+      state.origins[origin].encoding = e.target.value
+      var {match, csp, encoding} = state.origins[origin]
+      chrome.runtime.sendMessage({
+        message: 'origin.update',
+        origin,
+        options: {match, csp, encoding},
+      })
+      webRequest.update()
+      webRequest.permission(() => {
+        webRequest.register()
+      })
     },
   },
 }
 
+var webRequest = {
+  update: () => {
+    state.intercept = false
+    for (var key in state.origins) {
+      if (state.origins[key].csp || state.origins[key].encoding) {
+        state.intercept = true
+        break
+      }
+    }
+  },
+  permission: (done) => {
+    // ff: webRequest is required permission
+    if (/Firefox/.test(navigator.userAgent)) {
+      done()
+    }
+    else {
+      chrome.permissions[state.intercept ? 'request' : 'remove']({
+        permissions: ['webRequest', 'webRequestBlocking']
+      }, done)
+    }
+  },
+  register: () => {
+    chrome.runtime.sendMessage({
+      message: 'options.intercept',
+      intercept: state.intercept,
+    })
+  }
+}
+
 chrome.extension.isAllowedFileSchemeAccess((isAllowedAccess) => {
   state.file = /Firefox/.test(navigator.userAgent)
     ? true // ff: `Allow access to file URLs` option isn't available
@@ -123,6 +182,9 @@ init()
 var oncreate = {
   ripple: (vnode) => {
     mdc.ripple.MDCRipple.attachTo(vnode.dom)
+  },
+  textfield: (vnode) => {
+    mdc.textfield.MDCTextField.attachTo(vnode.dom)
   }
 }
 
@@ -132,8 +194,8 @@ var onupdate = {
       vnode.dom.classList.toggle('is-checked')
     }
   },
-  csp: (vnode) => {
-    if (vnode.dom.classList.contains('is-checked') !== state.csp) {
+  csp: (origin) => (vnode) => {
+    if (vnode.dom.classList.contains('is-checked') !== state.origins[origin].csp) {
       vnode.dom.classList.toggle('is-checked')
     }
   }
@@ -172,13 +234,16 @@ m.mount(document.querySelector('main'), {
             protocol + '://'
           )
         )),
-        m('.mdc-text-field m-textfield',
+        m('.mdc-text-field m-textfield', {
+          oncreate: oncreate.textfield,
+          },
           m('input.mdc-text-field__input', {
             type: 'text',
             value: state.origin,
             onchange: events.origin.name,
             placeholder: 'raw.githubusercontent.com'
-          })
+          }),
+          m('.mdc-line-ripple')
         ),
         m('button.mdc-button mdc-button--raised m-button', {
           oncreate: oncreate.ripple,
@@ -214,24 +279,8 @@ m.mount(document.querySelector('main'), {
           )
         ),
 
-        // csp
-        m('label.mdc-switch m-switch', {
-          onupdate: onupdate.csp,
-          title: 'Disable Content Security Policy (CSP)'
-          },
-          m('input.mdc-switch__native-control', {
-            type: 'checkbox',
-            checked: state.csp,
-            onchange: events.csp
-          }),
-          m('.mdc-switch__background', m('.mdc-switch__knob')),
-          m('span.mdc-switch-label',
-            'Disable ',
-            m('code', 'Content Security Policy'),
-          )
-        ),
-
-        m('ul.mdc-elevation--z2 m-list',
+        // origins
+        m('ul.m-list',
           Object.keys(state.origins).sort().map((origin) =>
             (
               (
@@ -241,33 +290,104 @@ m.mount(document.querySelector('main'), {
               )
               || origin !== 'file://' || null
             ) &&
-            m('li',
-              m('span', origin.replace(/^(\*|file|http(s)?).*/, '$1')),
-              m('span', origin.replace(/^(\*|file|http(s)?):\/\//, '')),
-              m('.mdc-text-field m-textfield', {
-                oncreate: oncreate.textfield
+            m('li.mdc-elevation--z2', {
+              class: state.origins[origin].expanded ? 'm-expanded' : null,
+              },
+              m('.m-summary', {
+                onclick: (e) => state.origins[origin].expanded = !state.origins[origin].expanded
                 },
-                m('input.mdc-text-field__input', {
-                  type: 'text',
-                  onkeyup: events.origin.update(origin),
-                  value: state.origins[origin],
+                m('.m-origin', origin),
+                m('.m-options',
+                  state.origins[origin].match !== state.match ? m('span', 'match') : null,
+                  state.origins[origin].csp ? m('span', 'csp') : null,
+                  state.origins[origin].encoding ? m('span', 'encoding') : null,
+                ),
+                m('i.material-icons', {
+                  class: state.origins[origin].expanded ? 'icon-arrow-up' : 'icon-arrow-down'
                 })
               ),
-              (origin !== 'file://' || null) &&
-              m('span',
-                m('button.mdc-button', {
-                  oncreate: oncreate.ripple,
-                  onclick: events.origin.refresh(origin),
-                  title: 'Refresh'
-                  },
-                  m('i.material-icons icon-refresh')
+              m('.m-content',
+                // match
+                m('.m-option m-match',
+                  m('.m-name', m('span', 'match')),
+                  m('.m-control',
+                    m('.mdc-text-field m-textfield', {
+                      oncreate: oncreate.textfield
+                      },
+                      m('input.mdc-text-field__input', {
+                        type: 'text',
+                        onkeyup: events.origin.match(origin),
+                        value: state.origins[origin].match,
+                      }),
+                      m('.mdc-line-ripple')
+                    )
+                  )
+                ),
+                // csp
+                (origin !== 'file://' || null) &&
+                m('.m-option m-csp',
+                  m('.m-name', m('span', 'csp')),
+                  m('.m-control',
+                    m('label.mdc-switch m-switch', {
+                      onupdate: onupdate.csp(origin),
+                      },
+                      m('input.mdc-switch__native-control', {
+                        type: 'checkbox',
+                        checked: state.origins[origin].csp,
+                        onchange: events.origin.csp(origin)
+                      }),
+                      m('.mdc-switch__background', m('.mdc-switch__knob')),
+                      m('span.mdc-switch-label',
+                        'Disable ',
+                        m('code', 'Content Security Policy'),
+                      )
+                    )
+                  )
+                ),
+                // encoding
+                (origin !== 'file://' || null) &&
+                m('.m-option m-encoding',
+                  m('.m-name', m('span', 'encoding')),
+                  m('.m-control',
+                    m('select.mdc-elevation--z2 m-select', {
+                      onchange: events.origin.encoding(origin),
+                      },
+                      m('option', {
+                        value: '',
+                        selected: state.origins[origin].encoding === ''
+                        },
+                        'auto'
+                      ),
+                      Object.keys(state.encodings).map((label) =>
+                        m('optgroup', {label}, state.encodings[label].map((encoding) =>
+                          m('option', {
+                            value: encoding,
+                            selected: state.origins[origin].encoding === encoding
+                            },
+                            encoding
+                          )
+                        ))
+                      )
+                    )
+                  )
                 ),
-                m('button.mdc-button', {
-                  oncreate: oncreate.ripple,
-                  onclick: events.origin.remove(origin),
-                  title: 'Remove'
-                  },
-                  m('i.material-icons icon-remove')
+                // refresh/remove
+                (origin !== 'file://' || null) &&
+                m('.m-footer',
+                  m('span',
+                    m('button.mdc-button mdc-button--raised m-button', {
+                      oncreate: oncreate.ripple,
+                      onclick: events.origin.refresh(origin)
+                      },
+                      'Refresh'
+                    ),
+                    m('button.mdc-button mdc-button--raised m-button', {
+                      oncreate: oncreate.ripple,
+                      onclick: events.origin.remove(origin)
+                      },
+                      'Remove'
+                    )
+                  )
                 )
               )
             )

+ 13 - 11
css/icons.css

@@ -10,48 +10,50 @@
 @media screen and (-webkit-min-device-pixel-ratio:0) {
   @font-face {
     font-family: 'fontello';
-    src: url('../font/fontello.svg?2723038#fontello') format('svg');
+    src: url('../font/fontello.svg?68292491#fontello') format('svg');
   }
 }
 */
-
+ 
  [class^="icon-"]:before, [class*=" icon-"]:before {
   font-family: "fontello";
   font-style: normal;
   font-weight: normal;
   speak: none;
-
+ 
   display: inline-block;
   text-decoration: inherit;
   width: 1em;
   margin-right: .2em;
   text-align: center;
   /* opacity: .8; */
-
+ 
   /* For safety - reset parent styles, that can break glyph codes*/
   font-variant: normal;
   text-transform: none;
-
+ 
   /* fix buttons height, for twitter bootstrap */
   line-height: 1em;
-
+ 
   /* Animation center compensation - margins should be symmetric */
   /* remove if not needed */
   margin-left: .2em;
-
+ 
   /* you can be more comfortable with increased icons size */
   /* font-size: 120%; */
-
+ 
   /* Font smoothing. That was taken from TWBS */
   -webkit-font-smoothing: antialiased;
   -moz-osx-font-smoothing: grayscale;
-
+ 
   /* Uncomment for 3D effect */
   /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
 }
-
+ 
 .icon-refresh:before { content: '\e801'; } /* '' */
 .icon-remove:before { content: '\e802'; } /* '' */
 .icon-firefox:before { content: '\e840'; } /* '' */
+.icon-arrow-down:before { content: '\f004'; } /* '' */
+.icon-arrow-up:before { content: '\f005'; } /* '' */
 .icon-github:before { content: '\f09b'; } /* '' */
-.icon-chrome:before { content: '\f268'; } /* '' */
+.icon-chrome:before { content: '\f268'; } /* '' */

BIN
css/icons.ttf


+ 120 - 25
css/options.css

@@ -165,38 +165,134 @@ footer .icon-hidden {
 .m-list {
   list-style: none;
   clear: both;
-  background: #fff;
   padding: 0;
   margin: 0;
 }
+
 .m-list li {
+  background: #fff;
   transition-duration: .28s;
   transition-timing-function: cubic-bezier(.4,0,.2,1);
   transition-property: background-color box-shadow;
-  height: auto;
-  min-height: 36px;
   border-bottom: 1px solid #ececec;
-  padding: 10px 20px;
 }
-.m-list li:last-child { border: 0; }
-.m-list li:hover { background: #eee; }
+.m-list li:first-child {
+
+}
+.m-list li:last-child {
+  margin-bottom: 0 !important;
+}
+.m-list li:hover:not(.m-expanded) { background: #ececec; }
 .m-list li:after { content: ''; display: block; clear: both; }
-.m-list li > * { display: inline-block; }
-.m-list li span {
-  font-size: 1rem;
-  line-height: 36px;
+.m-list li.m-expanded {
+  border: 0;
+  margin: 20px 0;
 }
-.m-list .m-textfield input { color: #a9a9a9; }
-.m-list .m-textfield input:focus { color: #1b1b1b; }
-.m-list button {
-  transition-duration: .28s;
-  transition-timing-function: cubic-bezier(.4,0,.2,1);
-  transition-property: background-color;
-  min-width: auto;
-  padding: 0 9px;
-  border-radius: 50%;
+
+.m-list .m-summary {
+  display: block;
+  height: 36px;
+  padding: 10px 20px 0 20px;
+  cursor: pointer;
+  position: relative;
+}
+.m-list .m-summary:after { content: ''; display: block; clear: both; }
+.m-list .m-summary .m-origin {
+  float: left;
+  font-family: monospace;
+  font-size: 16px;
+  line-height: 28px;
+}
+.m-list .m-summary .m-options {
+  float: right;
+  padding: 6px 25px 0 0;
+}
+.m-list .m-summary .m-options span {
+  background: #ececec;
+  font-size: 12px;
+  line-height: 15px;
+  padding: 2px 5px;
+  border-radius: 3px;
+  margin: 0 5px 0 0;
+}
+.m-list .m-summary i {
+  position: absolute;
+  top: 17px;
+  right: 17px;
+}
+
+.m-list .m-content {
+  /*TEST*/
+  display: none;
+  background: #fff;
+  padding: 0 20px 12px 20px;
+  position: relative;
+}
+.m-list .m-expanded .m-summary {
+  background: #ececec;
+}
+.m-list .m-expanded .m-origin {}
+.m-list .m-expanded .m-content {
+  display: block;
+}
+
+.m-list .m-content .m-option {
+  min-height: 50px;
+}
+.m-list .m-content .m-option:after { content: ''; display: block; clear: both; }
+.m-list .m-content .m-option .m-name {
+  float: left;
+  width: 10%;
+  font-size: 14px;
+  line-height: 50px;
+  text-align: right;
+}
+.m-list .m-content .m-option .m-name span {
+  font-size: 12px;
+  line-height: 15px;
+  text-transform: uppercase;
+  letter-spacing: .2px;
+}
+.m-list .m-content .m-option .m-control {
+  float: left;
+  width: 90%;
+}
+
+.m-list .m-content .m-option.m-match {}
+.m-list .m-content .m-option.m-match .m-control {
+  width: 80%;
+  margin: 0 0 0 15px;
+}
+.m-list .m-content .m-option.m-match .m-textfield {
+  width: 100%;
+  height: 43px;
+  margin: 0 0 7px 10px;
+  /*margin: 0;*/
+}
+.m-list .m-content .m-option.m-match input {
+  border-bottom: 1px solid #ececec !important;
+  padding: 15px 0 8px 0;
+}
+
+.m-list .m-content .m-option.m-csp {}
+.m-list .m-content .m-option.m-csp label {
+  margin: 12px 0 0 10px;
+}
+.m-list .m-content .m-option.m-encoding {}
+.m-list .m-content .m-option.m-encoding select {
+  width: 200px;
+  margin: 6px 0 0 25px;
+}
+
+.m-list .m-footer {
+  text-align: right;
+  position: absolute;
+  bottom: 20px;
+  right: 20px;
+}
+.m-list .m-footer .m-button:first-child {
+  margin: 0 20px 0 0;
 }
-.m-list button:hover { background: #cacaca; }
 
 
 /*file access*/
@@ -218,8 +314,12 @@ footer .icon-hidden {
 }
 .m-origins > .m-textfield:first-of-type {
   width: 400px;
+  height: auto !important;
   margin: 0 10px !important;
 }
+.m-origins > .m-textfield:first-of-type input {
+  padding-top: 10px;
+}
 .m-origins button:nth-of-type(2) { float: right; }
 .m-origins .m-select { width: 110px; }
 .m-origins .m-switch {
@@ -229,8 +329,3 @@ footer .icon-hidden {
   clear: both;
   margin: 20px 0 0 0;
 }
-.m-origins .m-list li > * { overflow: hidden; text-overflow: ellipsis; }
-.m-origins .m-list li > *:nth-child(1) { width: 7%; }
-.m-origins .m-list li > *:nth-child(2) { width: 22%; }
-.m-origins .m-list li > *:nth-child(3) { width: 62%; }
-.m-origins .m-list li > *:nth-child(4) { width: 9%; text-align: right; }