├── .gitignore ├── LICENSE ├── README.md ├── background.js ├── build-ext.sh ├── icons ├── loriko-64.png ├── loriko.svg └── penguin-mono.svg ├── lorify-ng.user.js ├── manifest.json ├── settings.html └── settings.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | dist -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 OpenA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Расширение для сайта [linux.org.ru](https://www.linux.org.ru/) 2 | Это форк проекта [lorify](https://bitbucket.org/b0r3d0m/lorify) использующий новые возможности движка форума такие как WebSocket. 3 | Является полностью универсальным, можно использовать и всё расширение целиком и отдельный юзерскрипт. 4 | 5 | ![lorify logo](https://github.com/OpenA/lorify-ng/blob/master/icons/loriko.svg?raw=true)| 6 | ------------ | ------------- 7 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | /* lorify-ng background script */ 2 | const defaults = Object.freeze({ // default settings 3 | 'Realtime Loader' : true, 4 | 'CSS3 Animation' : true, 5 | 'Delay Open Preview' : 50, 6 | 'Delay Close Preview' : 800, 7 | 'Desktop Notification' : 1, 8 | 'Preloaded Pages Count': 1, 9 | 'Picture Viewer' : 2, 10 | 'Scroll Top View' : true, 11 | 'Upload Post Delay' : 3, 12 | 'Code Block Short Size': 15, 13 | 'Code Highlight Style' : 0 14 | }); 15 | 16 | let notes = 0, codestyles = null, 17 | wss = 0, need_login = false; 18 | 19 | const settings = Object.assign({}, defaults); 20 | const openPorts = new Set; 21 | const isFirefox = navigator.userAgent.includes('Firefox'); 22 | const loadStore = isFirefox ? 23 | // load settings 24 | browser.storage.local.get() : new Promise(resolve => { 25 | chrome.storage.local.get(null, resolve) 26 | }); 27 | loadStore.then(items => { 28 | for (const key in items) { 29 | settings[key] = items[key]; 30 | } 31 | setNotifCheck(); 32 | }); 33 | 34 | chrome.notifications.onClicked.addListener(() => { 35 | const ismob = Number(settings['Desktop Notification']) === 2; 36 | openTab(`${ ismob ? '#' : 'lor://' }notifications`, 37 | ismob ? 'notes-show' : 'rel'); 38 | }); 39 | chrome.runtime.onConnect.addListener(port => { 40 | if (port.name === 'lory-wss') { 41 | chrome.alarms.clear('T-chk-notes'); 42 | } 43 | port.onMessage.addListener(messageHandler); 44 | port.onDisconnect.addListener(() => { 45 | openPorts.delete(port); 46 | for(const p of openPorts) { 47 | if (p.name === 'lory-wss') 48 | return; 49 | } 50 | setNotifCheck(5e4); 51 | }); 52 | openPorts.add(port); 53 | loadStore.then(() => port.postMessage({ action: 'connection-resolve', data: settings })); 54 | if (!codestyles) 55 | port.postMessage({ action: 'need-codestyles', data: null }); 56 | }); 57 | 58 | const setBadge = chrome.browserAction && chrome.browserAction.setBadgeText ? ( 59 | badge => { 60 | if ('setBadgeTextColor' in badge) { 61 | badge.setBadgeTextColor({ color: '#ffffff' }); 62 | } 63 | badge.setBadgeBackgroundColor({ color: '#3d96ab' }); 64 | badge.getBadgeText({}, label => { 65 | if (label > 0) 66 | notes = Number(label); 67 | }); 68 | return opts => { 69 | if ('text' in opts) badge.setBadgeText(opts); else 70 | if ('color' in opts) badge.setBadgeBackgroundColor(opts); 71 | } 72 | } 73 | )(chrome.browserAction) : () => void 0; 74 | 75 | chrome.alarms.onAlarm.addListener(getNotifications); 76 | 77 | function setNotifCheck(sec = 4e3) { 78 | if (settings['Desktop Notification']) { 79 | chrome.alarms.create('T-chk-notes', { 80 | when: Date.now() + sec, periodInMinutes: 4 81 | }); 82 | } 83 | } 84 | 85 | const queryScheme = isFirefox 86 | ? url => browser.tabs.query({ url }) 87 | : url => new Promise(res => void chrome.tabs.query({ url }, res)) 88 | 89 | const matchEmptyOr = (tabs, m_uri = '', m_rx = '') => new Promise(resolve => { 90 | 91 | const empty_Rx = new RegExp(m_rx ? m_rx : '^'+ 92 | (isFirefox ? 'about:(?:newtab|home)$' : 'chrome://.*(?:newtab|startpage)') + 93 | ''); 94 | 95 | let tab_id = -1; 96 | for (const { id, url } of tabs) { 97 | if (m_uri && url.includes(m_uri)) { 98 | tab_id = id; 99 | break; 100 | } else if (empty_Rx.test(url)) 101 | tab_id = id; 102 | } 103 | resolve(tab_id); 104 | }); 105 | 106 | function openTab(uri = '', action = '') { 107 | 108 | let schi = uri.search(/\?|\#/); 109 | if (schi === -1) 110 | schi = uri.length; 111 | 112 | const lor = uri.startsWith('lor:/') ? 5 : 0, 113 | path = uri.substring(lor, schi), 114 | orig = lor ? '://www.linux.org.ru'+ path : chrome.runtime.getURL('/settings.html'), 115 | href = lor ? 'https' + orig + uri.substring(schi) : orig + uri; 116 | 117 | for (const port of openPorts) { 118 | const { url, id } = port.sender.tab || ''; 119 | if (url && url.includes(orig)) { 120 | chrome.tabs.update(id, { active: true }); 121 | if (action === 'rel') { 122 | chrome.tabs.reload(id); 123 | } else 124 | port.postMessage({ action, data: uri.substring(lor) }); 125 | return; 126 | } 127 | } 128 | const fin = tab_id => { 129 | if (tab_id !== -1 ) { 130 | chrome.tabs.update(tab_id, { active: true, url: href }); 131 | chrome.tabs.reload(tab_id); 132 | } else 133 | chrome.tabs.create({ active: true, url: href }); 134 | } 135 | if (lor) { 136 | queryScheme(['*://www.linux.org.ru/', '*'+ orig +'*']).then( 137 | tabs => matchEmptyOr(tabs, orig, '^https?://www.linux.org.ru/?$') 138 | ).then(lor_id => { 139 | if(lor_id !== -1) { 140 | fin(lor_id); 141 | } else 142 | queryScheme().then(matchEmptyOr).then(fin); 143 | }); 144 | } else { 145 | queryScheme().then(tabs => matchEmptyOr(tabs, orig)).then(fin); 146 | } 147 | } 148 | 149 | function messageHandler({ action, data }, port) { 150 | // check 151 | switch (action) { 152 | case 'l0rNG-setts-reset': 153 | changeSettings(defaults); 154 | break; 155 | case 'l0rNG-setts-change': 156 | changeSettings(data, port); 157 | break; 158 | case 'l0rNG-notes-chk': 159 | chrome.alarms.clear('Q-chk-notes'); 160 | chrome.alarms.create('Q-chk-notes', { 161 | when: Date.now() + 1e3 162 | }); 163 | break; 164 | case 'l0rNG-codestyles': 165 | if(!codestyles) 166 | codestyles = data; 167 | break; 168 | case 'l0rNG-open-tab': 169 | openTab(data, 'scroll-to-comment'); 170 | break; 171 | case 'l0rNG-notes-set': 172 | if ( notes < data || notes > data ) { 173 | chrome.alarms.clear('Q-chk-notes'); 174 | updNoteStatus(data); 175 | } 176 | break; 177 | case 'l0rNG-extra-sets': 178 | if (codestyles) 179 | port.postMessage({ action: 'code-styles-list', data: codestyles }); 180 | if (notes) 181 | port.postMessage({ action: 'notes-count-update', data: notes }); 182 | } 183 | } 184 | 185 | function getNotifications(alm) { 186 | 187 | fetch('https://www.linux.org.ru/notifications-count', { 188 | credentials: 'same-origin', 189 | method: 'GET' 190 | }).then( 191 | response => { 192 | if (response.ok) { 193 | response.json().then( count => { 194 | if ( notes < count || notes > count ) 195 | updNoteStatus(count); 196 | }); 197 | } else if (response.status === 403) { 198 | chrome.alarms.clearAll(); 199 | need_login = true; 200 | } 201 | } 202 | ); 203 | } 204 | 205 | function updNoteStatus(count = 0) { 206 | if ( count > notes && settings['Desktop Notification'] ) { 207 | chrome.notifications.create('lorify-ng', { 208 | type : 'basic', 209 | title : 'LINUX.ORG.RU', 210 | message : count +' новых сообщений', 211 | iconUrl : 'icons/penguin-mono.svg' 212 | }); 213 | } 214 | setBadge({ text: (notes = count) ? count.toString() : '' }); 215 | for (const port of openPorts) { 216 | port.postMessage({ action: 'notes-count-update', data: count }); 217 | } 218 | } 219 | function changeSettings(newSetts, exclupe = null) { 220 | let hasChecks = 0; 221 | for (const port of openPorts) { 222 | hasChecks += Number(port.name === 'lory-wss'); 223 | if ( port !== exclupe ) 224 | port.postMessage({ action: 'settings-change', data: newSetts }); 225 | } 226 | Object.assign(settings, newSetts); 227 | if (!hasChecks) 228 | setNotifCheck(); 229 | chrome.storage.local.set(newSetts); 230 | } 231 | -------------------------------------------------------------------------------- /build-ext.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | SRC="lorify-ng.user.js settings.html settings.js background.js LICENSE icons/*" 4 | ARC="dist/lorify-ng.zip" 5 | VER="2" 6 | 7 | for i in "$@"; do 8 | case $i in 9 | --help | -h) 10 | echo "" 11 | echo " build-ext.sh [option]\n" 12 | echo " -v3 Manifest V3" 13 | echo " -v2 Manifest V2 (default)" 14 | echo "" 15 | exit 16 | ;; 17 | -v3) 18 | VER="3" 19 | ;; 20 | *) 21 | # unknown option 22 | ;; 23 | esac 24 | done 25 | 26 | mkdir -p dist 27 | echo "\nbuilding:\033[0;9$VER;49m WebExt Manifest V$VER \033[0m" 28 | 29 | if [ "$VER" = "2" ]; then 30 | sed -e 's/"manifest_version":.*3/"manifest_version": 2/' \ 31 | -e 's/"action"/"browser_action"/' \ 32 | -e 's/"service_worker"/"scripts"/' \ 33 | -e 's/"background.js"/["background.js"], "persistent": true/' \ 34 | -e 's/\],.*"permissions":.*\[/,/' \ 35 | -e 's/"host_permissions"/"permissions"/' \ 36 | manifest.json > dist/manifest.json 37 | zip -9 -jm $ARC dist/manifest.json 38 | else 39 | SRC="manifest.json $SRC" 40 | fi 41 | 42 | zip -9 -T $ARC $SRC 43 | -------------------------------------------------------------------------------- /icons/loriko-64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenA/lorify-ng/2aae9c11020631b8479b55925000e99d2d8cca7b/icons/loriko-64.png -------------------------------------------------------------------------------- /icons/loriko.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | Penguin named Loriko 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | Penguin named Loriko 28 | 22.12.2021 29 | 30 | 31 | OpenA 32 | 33 | 34 | 35 | 36 | 37 | 58 | 60 | 62 | 64 | 66 | 67 | 72 | 74 | 80 | 81 | 83 | 84 | 89 | 97 | 116 | 121 | 125 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /icons/penguin-mono.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 50 | 52 | 57 | 62 | 80 | 83 | 87 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "lorify-ng", 4 | "version": "3.3.8", 5 | "description": "Расширение для сайта linux.org.ru поддерживающее загрузку комментариев через технологию WebSocket, а так же уведомления об ответах через системные оповещения и многое другое.", 6 | "options_ui": { 7 | "page": "settings.html" 8 | }, 9 | "action": { 10 | "default_icon": "icons/loriko-64.png", 11 | "default_popup": "settings.html" 12 | }, 13 | "background": { 14 | "service_worker": "background.js" 15 | }, 16 | "icons": { 17 | "64": "icons/loriko-64.png" 18 | }, 19 | "content_scripts": [{ 20 | "run_at": "document_start", 21 | "matches": ["*://www.linux.org.ru/*"], 22 | "js": ["lorify-ng.user.js"] 23 | }], 24 | "host_permissions": [ 25 | "*://www.linux.org.ru/*" 26 | ], "permissions": [ 27 | "notifications", 28 | "storage", 29 | "alarms", 30 | "tabs" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | lorify-ng Options 7 | 173 | 174 | 175 |
176 | 177 |
178 | Автоподгрузка комментариев: 179 | 180 | 181 | 182 |
183 |
184 | Укорачивать блоки кода свыше: 185 | 186 | 187 | 188 |
189 |
190 | Стиль подсветки кода: 191 | 192 | 193 | 194 |
195 |
196 | Задержка появления / исчезновения превью: 197 | 198 | 199 | / 200 | 201 | 202 |
203 |
204 | Предзагружаемых страниц: 205 | 206 | 207 | 208 |
209 |
210 | Оповещения на рабочий стол: 211 | 212 |
213 | 216 | 219 | 222 |
223 |
224 |
225 |
226 | Просмотр картинок: 227 | 228 | 233 | 234 |
235 |
236 | Задержка перед отправкой: 237 | 238 | 239 | 240 | 241 |
242 |
243 | Перемещать в начало страницы: 244 | 245 | 246 |
247 |
248 | CSS анимация: 249 | 250 | 251 | 252 | 253 |
254 |
255 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | 2 | const loryform = document.getElementById('loryform'); 3 | const n_count = document.getElementById('note-count'); 4 | const note_lst = document.getElementById('note-list'); 5 | const rst_btn = document.getElementById('reset-settings'); 6 | 7 | let busy_id = -1, not_ld = true, 8 | cnt_new = 0, 9 | my_connect = new Promise(createPort); 10 | 11 | const showNotifications = () => { 12 | history.replaceState(null, null, location.pathname +'#notifications'); 13 | note_lst.hidden = false; 14 | if (not_ld) 15 | updNotifications(cnt_new); 16 | } 17 | 18 | if (location.hash === '#notifications') 19 | note_lst.hidden = false; 20 | 21 | loryform.addEventListener('animationend', () => { 22 | loryform.classList.remove('save-msg'); 23 | }); 24 | loryform.addEventListener('change', ({ target }) => { 25 | if (busy_id === -1) 26 | onValueChange(target); 27 | }); 28 | loryform.addEventListener('input', ({ target }) => { 29 | clearTimeout(busy_id); 30 | busy_id = setTimeout(() => { 31 | busy_id = -1; 32 | onValueChange(target); 33 | }, 750); 34 | }); 35 | 36 | rst_btn.addEventListener('click', () => applyAnim('l0rNG-setts-reset', null, true)); 37 | n_count.addEventListener('click', showNotifications); 38 | note_lst.addEventListener('click', e => { 39 | let el = e.target; 40 | switch (el.id) { 41 | case 'go-back' : note_lst.hidden = true; history.replaceState(null, null, location.pathname); 42 | case 'do-wait' : break; 43 | case 'reset-notes': 44 | if ('reset_form' in document.forms) { 45 | el.id = 'do-wait'; 46 | fetch('https://www.linux.org.ru/notifications-reset', { 47 | credentials: 'same-origin', 48 | method: 'POST', 49 | body: new FormData( document.forms.reset_form ) 50 | }).then(({ ok }) => { 51 | if (ok) { 52 | applyAnim('l0rNG-notes-set', 0); 53 | document.forms.reset_form.remove(); 54 | } 55 | el.id = 'reset-notes'; 56 | }); 57 | } 58 | break; 59 | } 60 | }); 61 | 62 | function createPort(resolve) { 63 | const port = chrome.runtime.connect(); 64 | port.onMessage.addListener(({ action, data }) => { 65 | switch (action) { 66 | case 'notes-show': 67 | showNotifications(); 68 | break; 69 | case 'code-styles-list': 70 | const input = loryform.elements['Code Highlight Style']; 71 | for (const cname of data) { 72 | input.appendChild( document.createElement('option') ).textContent = cname; 73 | } 74 | break; 75 | case 'notes-count-update': 76 | n_count.setAttribute('cnt-new', data); 77 | n_count.hidden = !(cnt_new = Number(data)); 78 | if (!note_lst.hidden && not_ld) 79 | updNotifications(cnt_new); 80 | break; 81 | case 'connection-resolve': 82 | port.postMessage({ action: 'l0rNG-extra-sets', data: null }) 83 | resolve(port); 84 | case 'settings-change': 85 | setValues(data); 86 | } 87 | }); 88 | port.onDisconnect.addListener(() => { 89 | my_connect = null; 90 | }); 91 | } 92 | 93 | function applyAnim(action = '', data = null, anim = false) { 94 | if (anim) 95 | loryform.classList.add('save-msg'); 96 | if(!my_connect) 97 | my_connect = new Promise(createPort) 98 | my_connect.then( 99 | port => port.postMessage({ action, data }) 100 | ); 101 | } 102 | 103 | function updNotifications(count = 0) { 104 | let tr_lst = note_lst.lastElementChild.children, 105 | do_upd = count > 0 && tr_lst.length < count; 106 | 107 | if (tr_lst.length) { 108 | for(let i = do_upd ? 0 : count; tr_lst[i];) 109 | tr_lst[i].remove(); 110 | } 111 | if (do_upd) { 112 | not_ld = false; 113 | fetch('https://www.linux.org.ru/notifications', { 114 | credentials: 'same-origin', 115 | method: 'GET' 116 | }).then(res => { 117 | if (res.ok) 118 | res.text().then(pullNotes); 119 | not_ld = true; 120 | }); 121 | } 122 | } 123 | 124 | function onValueChange(input) { 125 | const changes = {}; 126 | let { name, type, value, min, max } = input; 127 | if (min && Number(value) < Number(min)) input.value = value = min; else 128 | if (max && Number(value) > Number(max)) input.value = value = max; 129 | changes[name] = ( 130 | type === 'select-one' ? input.selectedIndex : 131 | type === 'checkbox' ? input.checked : Number(value) 132 | ); 133 | applyAnim('l0rNG-setts-change', changes, true); 134 | } 135 | 136 | function setValues(items) { 137 | for (const name in items) { 138 | const i_el = loryform.elements[name], type = i_el.type, 139 | param = type === 'select-one' ? 'selectedIndex' : 140 | type === 'checkbox' ? 'checked' : 'value'; 141 | i_el[param] = type ? items[name] : Number(items[name]); 142 | } 143 | } 144 | 145 | function pullNotes(html) { 146 | 147 | const doc = new DOMParser().parseFromString(html, 'text/html'), 148 | items = Array.from(doc.querySelector('.notifications').children), 149 | list = note_lst.lastElementChild, 150 | limit = cnt_new > items.length ? items.length : cnt_new; 151 | 152 | const new_rf = doc.forms.reset_form; 153 | const old_rf = document.forms.reset_form; 154 | 155 | if (new_rf) { 156 | new_rf.hidden = true; 157 | if (old_rf) { 158 | document.body.replaceChild(new_rf, old_rf); 159 | } else 160 | document.body.appendChild(new_rf); 161 | } 162 | 163 | for (let i = 0; i < limit; i++) { 164 | const item = items[i], 165 | title = item.children[1], 166 | icon = item.children[0], type = icon.firstElementChild.firstElementChild, 167 | detail = item.children[2], tags = detail.firstElementChild.firstElementChild, 168 | info = item.children[3], time = info.firstElementChild.lastElementChild; 169 | 170 | let who = time.previousSibling, 171 | tip = '', chr = 'cек', usr, 172 | num = detail.innerText; 173 | 174 | if (tags && tags.className === 'reactions') { 175 | usr = time.parentNode.insertBefore(tags, time); 176 | usr.append(who); 177 | } else { 178 | if (!time.previousElementSibling){ 179 | usr = time.parentNode.insertBefore(document.createElement('span'), time); 180 | usr.append(who); 181 | } else 182 | usr = time.previousElementSibling; 183 | if (tags && tags.className === 'tag') 184 | usr.append(...detail.firstElementChild.children); 185 | } 186 | item.className = 'note-item'; 187 | info.className = 'note-item-info'; 188 | title.className = 'note-item-topic'; 189 | usr.className = 'note-item-user'; 190 | time.className = 'note-item-time'; 191 | 192 | let sec = Math.floor((Date.now() - new Date( time.dateTime )) / 1000); 193 | if (sec >= 86400) chr = 'дн' , sec = Math.floor(sec / 86400); else 194 | if (sec >= 3600 ) chr = 'ч' , sec = Math.floor(sec / 3600); else 195 | if (sec >= 60 ) chr = 'мин', sec = Math.floor(sec / 60) % 60; 196 | 197 | if (type) { 198 | tip = type.title; 199 | if (tip.endsWith('удалено')) { 200 | tip = 'Удалено'; 201 | who.textContent = num; 202 | usr.classList.add('modmes'); 203 | } else if (type.classList.contains('icon-user-color')) 204 | tip = 'Приглашён'; 205 | } else if (num > 0) { 206 | tip = 'Новое'; 207 | who.textContent = `${num}💬\n`; 208 | } 209 | time.textContent = sec, 210 | time.setAttribute('data-chr', chr); 211 | time.parentNode.setAttribute('data-tip', tip); 212 | 213 | icon.remove(), detail.remove(); 214 | item.append(info, title); 215 | list.append(item); 216 | item.onclick = e => { 217 | e.preventDefault() 218 | e.stopPropagation(); 219 | applyAnim('l0rNG-open-tab', 'lor:/'+ item.getAttribute('href')); 220 | } 221 | } 222 | } 223 | --------------------------------------------------------------------------------