├── .gitignore ├── .github ├── renovate.json ├── build_done.sh ├── workflows │ ├── release.yml │ └── ci.yml └── install.sh ├── src ├── types.ts ├── core │ ├── inject-custom-root-style.ts │ ├── refined-image-gallery.ts │ ├── index.ts │ ├── listen-rooms-panel-drag-event.ts │ ├── theme-listener.ts │ ├── file-changes-listener.ts │ └── modify-chat-box.ts ├── functions │ ├── index.ts │ ├── enhance-stickers-display.ts │ ├── merge-same-user-message.ts │ ├── fix-message-content-width.ts │ ├── special-username-color.ts │ └── better-image-display.ts ├── main.ts └── utils.ts ├── styles ├── global.scss ├── modal.scss ├── textarea.scss ├── submit-modal.scss ├── stickers-panel.scss ├── darkmode.scss ├── room-group.scss └── chatbox.scss ├── tsconfig.json ├── style.scss ├── rollup.config.mjs ├── config.js ├── types.d.ts ├── replace.sh ├── addon.js ├── package.json ├── dev-server.js ├── README.md ├── pnpm-lock.yaml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist 3 | node_modules -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "enabled": false 3 | } -------------------------------------------------------------------------------- /.github/build_done.sh: -------------------------------------------------------------------------------- 1 | cp addon.js ./dist 2 | cp config.js ./dist 3 | pnpm build:css 4 | cp ./.github/install.sh ./dist 5 | echo Done. -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface IMessageUser { 2 | id: string; 3 | name: string; 4 | } 5 | 6 | export type IMessageUserList = IMessageUser[]; -------------------------------------------------------------------------------- /styles/global.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: var(--tg-panel-background) !important; 3 | } 4 | 5 | body { 6 | --tg-panel-background: rgb(255, 255, 255); 7 | --tg-chatbox-background: rgb(245 245 245); 8 | } 9 | 10 | * { 11 | font-family: "PingFang SC", "Helvetica Neue", Helvetica, Arial, sans-serif !important; 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "lib": ["esnext", "DOM"], 6 | "moduleResolution": "node", 7 | "esModuleInterop": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "skipDefaultLibCheck": true, 13 | }, 14 | } -------------------------------------------------------------------------------- /style.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * icalingua++ Custom Style 3 | * @references Telegram Desktop Style 4 | */ 5 | 6 | @import "styles/global.scss"; 7 | @import "styles/room-group.scss"; 8 | @import "styles/chatbox.scss"; 9 | @import "styles/textarea.scss"; 10 | @import "styles/stickers-panel.scss"; 11 | 12 | @import "styles/modal.scss"; 13 | // @import "styles/submit-modal.scss"; 14 | 15 | @import "styles/darkmode.scss"; 16 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import nodeResolve from "@rollup/plugin-node-resolve"; 2 | import typescript from "rollup-plugin-ts"; 3 | import { defineConfig } from "rollup"; 4 | import { minify } from "rollup-plugin-esbuild"; 5 | 6 | export default defineConfig({ 7 | input: [ 8 | "src/main.ts", 9 | ], 10 | output: { 11 | dir: "dist", 12 | format: "module", 13 | }, 14 | plugins: [ 15 | nodeResolve(), 16 | typescript(), 17 | minify(), 18 | ], 19 | 20 | }) -------------------------------------------------------------------------------- /src/core/inject-custom-root-style.ts: -------------------------------------------------------------------------------- 1 | import { createConsole, getRoomsPanelWidth } from "../utils"; 2 | 3 | export function injectCustomRootStyle() { 4 | const roomsPanelWidth = getRoomsPanelWidth(); 5 | 6 | const style = document.createElement('style'); 7 | style.id = 'custom-root-style'; 8 | style.innerHTML = ` 9 | :root { 10 | --rooms-panel-width: ${roomsPanelWidth}; 11 | } 12 | `; 13 | document.head.appendChild(style); 14 | createConsole('InjectCustomRootStyle', 'Custom root style injected'); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/core/refined-image-gallery.ts: -------------------------------------------------------------------------------- 1 | import { injectCSS, injectScript } from '../utils.js'; 2 | 3 | export function refinedImageGallery(){ 4 | injectScript("https://unpkg.com/photoswipe@5.3.7/dist/photoswipe-lightbox.esm.js", false, () => {}); 5 | // const lightbox = new PhotoSwipeLightbox({ 6 | // gallery: '#my-gallery', 7 | // children: 'a', 8 | // pswpModule: () => import('https://unpkg.com/photoswipe'), 9 | // }); 10 | // lightbox.init(); 11 | injectCSS("https://unpkg.com/photoswipe@5.3.7/dist/photoswipe.css"); 12 | } -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | window.theme = { 2 | chatbox: [ 3 | "merge-same-user-message", 4 | "enhance-stickers-display", 5 | "not-exist-function", 6 | "special-username-color", 7 | "better-image-display", 8 | "fix-message-content-width", 9 | ], 10 | dev: false, 11 | core: [ 12 | "inject-custom-root-style", 13 | "listen-rooms-panel-drag-event", 14 | "modify-chat-box-interval", 15 | "refined-image-gallery", 16 | // "file-changes-listener", // 建议只在开发时开启 17 | "theme-listener", 18 | ], 19 | manual: true, 20 | log: true, 21 | } -------------------------------------------------------------------------------- /types.d.ts: -------------------------------------------------------------------------------- 1 | import { CoresKey } from "./src/core" 2 | import { Functions, FunctionsKey } from "./src/functions" 3 | 4 | declare global { 5 | interface Window { 6 | theme: { 7 | // Auto generated 8 | location: string, 9 | // From settings (Auto generated) 10 | theme: "auto" | "light" | "dark", 11 | localImageViewerByDefault: boolean, 12 | systemTheme: "light" | "dark", 13 | // Manual control 14 | dev: boolean, 15 | chatbox: FunctionsKey[], 16 | core: CoresKey[], 17 | manual: boolean, // 是否手动控制功能启动 18 | log: boolean, // 是否打印日志 19 | } 20 | } 21 | } 22 | 23 | export { } -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | import { fileChangesListener } from "./file-changes-listener"; 2 | import { injectCustomRootStyle } from "./inject-custom-root-style"; 3 | import { listenRoomsPanelDragEvent } from "./listen-rooms-panel-drag-event"; 4 | import { modifyChatBoxInterval } from "./modify-chat-box"; 5 | import { themeListener } from "./theme-listener"; 6 | 7 | export const Cores = { 8 | "inject-custom-root-style": injectCustomRootStyle, 9 | "listen-rooms-panel-drag-event": listenRoomsPanelDragEvent, 10 | "modify-chat-box-interval": modifyChatBoxInterval, 11 | "file-changes-listener": fileChangesListener, 12 | "theme-listener": themeListener, 13 | }; 14 | 15 | export type CoresKey = keyof typeof Cores; -------------------------------------------------------------------------------- /src/core/listen-rooms-panel-drag-event.ts: -------------------------------------------------------------------------------- 1 | import { createConsole, getRoomsPanelWidth, getRootStyle, setRootStyle } from '../utils.js'; 2 | 3 | export function listenRoomsPanelDragEvent() { 4 | const elMain = document.querySelector('div.el-main.multipane.layout-v'); 5 | if (elMain) { 6 | elMain.addEventListener('mouseup', () => { 7 | const roomsPanelWidth = getRoomsPanelWidth(); 8 | const customRoomsPanelWidth = getRootStyle('rooms-panel-width'); 9 | if (roomsPanelWidth !== customRoomsPanelWidth) { 10 | setRootStyle('rooms-panel-width', roomsPanelWidth); 11 | } 12 | }); 13 | createConsole('ListenRoomsPanelDragEvent', 'Rooms panel drag event listening'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/functions/index.ts: -------------------------------------------------------------------------------- 1 | import { betterImageDisplay } from "./better-image-display"; 2 | import { enhanceStickersDisplay } from "./enhance-stickers-display"; 3 | import { fixMessageContentWidth } from "./fix-message-content-width"; 4 | import { mergeSameUserMessage } from "./merge-same-user-message"; 5 | import { specialUsernameColor } from "./special-username-color"; 6 | 7 | export const Functions = { 8 | "merge-same-user-message": mergeSameUserMessage, 9 | "better-image-display": betterImageDisplay, 10 | "special-username-color": specialUsernameColor, 11 | "enhance-stickers-display": enhanceStickersDisplay, 12 | "fix-message-content-width": fixMessageContentWidth, 13 | } 14 | 15 | export type FunctionsKey = keyof typeof Functions; -------------------------------------------------------------------------------- /replace.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Check if the system is macOS 3 | if [ "$(uname)" == "Darwin" ]; then 4 | pnpm build 5 | cp ./dist/main.js ~/Library/Application\ Support/icalingua 6 | cp ./dist/style.css ~/Library/Application\ Support/icalingua 7 | cp addon.js ~/Library/Application\ Support/icalingua 8 | cp config.js ~/Library/Application\ Support/icalingua 9 | echo "Done." 10 | # Check if the system is Linux 11 | elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 12 | pnpm build 13 | cp ./dist/style.css ~/.config/icalingua 14 | cp addon.js ~/.config/icalingua 15 | cp ./dist/main.js ~/.config/icalingua 16 | cp config.js ~/.config/icalingua 17 | echo "Done." 18 | 19 | else 20 | echo "Sorry, your system is not supported." 21 | fi 22 | -------------------------------------------------------------------------------- /addon.js: -------------------------------------------------------------------------------- 1 | window.onload = async function () { 2 | const scriptElement = document.head.getElementsByTagName('script')[0].src; 3 | const settings = await require('electron').ipcRenderer.invoke('getSettings'); 4 | const refineScript = document.createElement('script'); 5 | refineScript.type = 'module'; 6 | refineScript.src = scriptElement.replace('addon.js', 'main.js'); 7 | const configScript = document.createElement('script'); 8 | configScript.src = scriptElement.replace('addon.js', 'config.js'); 9 | document.head.appendChild(configScript); 10 | configScript.onload = function () { 11 | 12 | window.theme = { 13 | ...window.theme, 14 | location: decodeURIComponent(scriptElement.replace('addon.js', '').replace('file:///', '')), 15 | theme: settings.theme, 16 | localImageViewerByDefault: settings.localImageViewerByDefault, 17 | }; 18 | 19 | document.head.appendChild(refineScript); 20 | }; 21 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "icalingua-theme-telegram", 3 | "version": "1.0.2", 4 | "description": "A Telegram-like theme based on icalingua++", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "node dev-server.js", 8 | "build": "rollup -c && npm run hook:done", 9 | "build:css": "sass style.scss ./dist/style.css", 10 | "typecheck": "tsc --noEmit", 11 | "hook:done": "sh .github/build_done.sh" 12 | }, 13 | "keywords": [ 14 | "telegram", 15 | "theme", 16 | "icalingua", 17 | "icalingua++" 18 | ], 19 | "author": "wibus-wee ", 20 | "license": "AGPL-3.0", 21 | "devDependencies": { 22 | "@rollup/plugin-node-resolve": "^15.1.0", 23 | "@types/node": "^20.3.3", 24 | "chalk": "^5.3.0", 25 | "consola": "^3.2.2", 26 | "rollup": "^3.26.1", 27 | "rollup-plugin-esbuild": "^5.0.0", 28 | "rollup-plugin-ts": "^3.2.0", 29 | "sass": "^1.63.6", 30 | "typescript": "^5.1.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release CI 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | permissions: 8 | write-all 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set node 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: latest 19 | 20 | - name: Setup 21 | run: npm i -g @antfu/ni 22 | 23 | - name: Install 24 | run: nci 25 | 26 | - name: Build 27 | run: nr build 28 | 29 | - name: Upload 30 | uses: actions/upload-artifact@v2 31 | with: 32 | name: icalingua-theme-telegram.zip 33 | path: dist 34 | - name: Zip 35 | run: zip -r icalingua-theme-telegram.zip dist 36 | - name: Upload a Build Artifact 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: icalingua-theme-telegram.zip 40 | path: icalingua-theme-telegram.zip -------------------------------------------------------------------------------- /src/functions/enhance-stickers-display.ts: -------------------------------------------------------------------------------- 1 | import { IMessageUserList } from "../types"; 2 | 3 | export function enhanceStickersDisplay(messageUserList: IMessageUserList) { 4 | messageUserList.forEach((message) => { 5 | const messageElement = document.getElementById(message.id); 6 | if (!messageElement) { 7 | return; 8 | } 9 | 10 | // .vac-message-card > div > svg 这是 Super Stickers 的标识。 11 | // 其实需要做的不多,但是和 BetterImageDisplay 有点不一样,这次我选择通过加减 class 来实现。 12 | const superSticker = messageElement.querySelector( 13 | ".vac-message-container .vac-message-card > div > svg" 14 | ); 15 | if (!superSticker) { 16 | return; 17 | } 18 | const card = messageElement.querySelector(".vac-message-card") as HTMLElement; 19 | if (!card) { return; } 20 | card.attributeStyleMap.set("--tg-chatbox-background", "transparent"); // 透明背景即可 21 | // 丢掉那个垃圾 vac-message-content,一天天的使用最新版手机QQ体验新功能🤬 22 | const content = messageElement.querySelector(".vac-message-content"); 23 | if (!content) { 24 | return; 25 | } 26 | content.remove(); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/core/theme-listener.ts: -------------------------------------------------------------------------------- 1 | import { createConsole } from "../utils"; 2 | 3 | export function themeListener() { 4 | // https://github.com/wibus-wee/icalingua-theme-telegram/issues/21#issuecomment-1621126078 5 | setInterval(async () => { 6 | const settings = await require('electron').ipcRenderer.invoke('getSettings'); 7 | window.theme.theme = settings.theme; 8 | const theme = settings.theme; 9 | if (theme === 'auto') { 10 | if (!window.theme.systemTheme) { 11 | window.theme.systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 12 | } 13 | document.documentElement.setAttribute("theme", window.theme.systemTheme); 14 | } else { 15 | document.documentElement.setAttribute("theme", theme); 16 | } 17 | }, 500); 18 | 19 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { 20 | const newColorScheme = e.matches ? 'dark' : 'light'; 21 | if (window.theme.theme === 'auto') { 22 | window.theme.systemTheme = newColorScheme; 23 | document.documentElement.setAttribute('theme', newColorScheme); 24 | } 25 | }); 26 | 27 | createConsole('ThemeListener', 'Theme listener listening'); 28 | } 29 | -------------------------------------------------------------------------------- /.github/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Define the destination directory path 4 | if [ "$(uname)" == "Darwin" ]; then 5 | destination_dir=~/Library/Application\ Support/icalingua 6 | elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 7 | destination_dir=~/.config/icalingua 8 | else 9 | echo "Sorry, your system is not supported." 10 | exit 1 11 | fi 12 | 13 | # Copy the files to the destination directory 14 | cp_files() { 15 | cp "$1" "$destination_dir" 16 | } 17 | 18 | # Copy the file and prompt for overwrite if necessary (only for config.js) 19 | copy_file_with_prompt() { 20 | local file="$1" 21 | local dest_path="$destination_dir/$file" 22 | if [ "$file" == "config.js" ]; then 23 | if [ -f "$dest_path" ]; then 24 | echo "The file 'config.js' already exists in the destination directory." 25 | read -p "Do you want to overwrite it? (y/n): " answer 26 | if [ "$answer" == "y" ]; then 27 | cp_files "$file" 28 | fi 29 | else 30 | cp_files "$file" 31 | fi 32 | else 33 | cp_files "$file" 34 | fi 35 | } 36 | 37 | # Copy the files based on the system type 38 | if [ -n "$destination_dir" ]; then 39 | copy_file_with_prompt "main.js" 40 | copy_file_with_prompt "style.css" 41 | copy_file_with_prompt "addon.js" 42 | copy_file_with_prompt "config.js" 43 | echo "Done." 44 | fi 45 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Cores } from "./core"; 2 | import { Functions } from "./functions"; 3 | import { createConsole, createConsoleGroup, getRoomsPanelWidth } from "./utils"; 4 | 5 | function init() { 6 | // #app > .vac-col-messages 是用于判断是否在「转发聊天页面」页面 7 | if (!getRoomsPanelWidth() && !document.querySelector("#app > .vac-col-messages")) { 8 | setTimeout(init, 1000); 9 | createConsole("ADDON", "rooms-panel not found, try again after 1s"); 10 | return; 11 | } 12 | if (!window.theme.manual || window.theme.dev) { 13 | // 如果不是手动控制功能启动, dev 模式下也会全部启动 14 | window.theme.core = Object.keys(Cores) as (keyof typeof Cores)[]; 15 | window.theme.chatbox = Object.keys(Functions) as (keyof typeof Functions)[]; 16 | } 17 | createConsoleGroup( 18 | "Init", 19 | "These Core Functions will be executed:", 20 | window.theme.core.map((f) => { 21 | // 将短横线转换为驼峰 22 | const text = f.replace(/-(\w)/g, function (_all, letter) { 23 | return letter.toUpperCase(); 24 | }) as keyof typeof Cores; 25 | const isCoreExist = Cores[f] !== undefined; 26 | if (!isCoreExist) { 27 | window.theme.core = window.theme.core.filter((c) => c !== f); 28 | } 29 | return { 30 | text: `${text} => ${isCoreExist ? "✅" : "❌"}`, 31 | type: isCoreExist ? "log" : "warn", 32 | }; 33 | }) 34 | ); 35 | Promise.all( 36 | window.theme.core.map((fn) => { 37 | return Cores[fn](); 38 | }) 39 | ); 40 | } 41 | init(); 42 | -------------------------------------------------------------------------------- /src/core/file-changes-listener.ts: -------------------------------------------------------------------------------- 1 | import { createConsole } from "../utils"; 2 | 3 | const fs = require('fs'); 4 | 5 | const watcher = ["addon.js", "main.js", "style.css"] 6 | 7 | export function fileChangesListener() { 8 | // The current simple idea is to use node:fs to listen for changes to all files, and once a file change is detected, it automatically copies that file to the data directory and requires the page to reload. 9 | // If it is a css file, re-append the CSS file. 10 | // https://github.com/wibus-wee/icalingua-theme-telegram/issues/15 11 | if (!window.theme.dev) return; 12 | fs.watch(window.theme.location, { recursive: true }, (eventType: any, filename: string) => { 13 | if (watcher.includes(filename) || filename.includes("telegram-theme")) { 14 | // console.log(`${filename} has been changed`); 15 | if (filename.endsWith('.css')) { 16 | // console.log(`${filename} has been updated`) 17 | const oldStyleElement = document.querySelector(`link`); 18 | if (oldStyleElement) { 19 | oldStyleElement.remove(); 20 | // console.log(`Removed old style element`) 21 | } 22 | const cssFile = window.theme.location + filename; 23 | const styleElement = document.createElement('link'); 24 | styleElement.rel = 'stylesheet'; 25 | styleElement.href = cssFile; 26 | document.head.appendChild(styleElement); 27 | // console.log(`Appended new style element`) 28 | } else { 29 | location.reload(); 30 | } 31 | } 32 | }) 33 | createConsole('FileChangesListener', 'File changes listener listening'); 34 | 35 | } -------------------------------------------------------------------------------- /src/functions/merge-same-user-message.ts: -------------------------------------------------------------------------------- 1 | import { IMessageUser, IMessageUserList } from "../types"; 2 | 3 | /** 4 | * 在同一用户连续发送的消息中,只保留最后一个头像 5 | */ 6 | export function mergeSameUserMessage(messageUserList: IMessageUserList) { 7 | let lastUser = null as null | IMessageUser; 8 | messageUserList.forEach((messageUser, index) => { 9 | if (messageUser.name === 'current') { 10 | return; 11 | } 12 | // 接着我们需要遍历这个列表,如果发现连续的用户是同一个人 13 | // 那么我们就需要把前面的头像都隐藏掉,只保留最后一个头像 14 | // 如果 lastUser 为空,那么我们就把 lastUser 设置为当前用户,然后继续循环 15 | if (!lastUser) { 16 | lastUser = messageUser; 17 | return; 18 | } 19 | // 如果 lastUser 和当前用户不一样,那么我们就把 lastUser 设置为当前用户,然后继续循环 20 | if (lastUser.name !== messageUser.name) { 21 | lastUser = messageUser; 22 | return; 23 | } 24 | // 如果 lastUser 和当前用户一样,那么我们就需要隐藏掉消息的头像 25 | const message = document.getElementById(messageUser.id); 26 | if (!message) { 27 | return; 28 | } 29 | if (messageUserList[index - 1].name === 'current') { 30 | return; 31 | } 32 | const name = message.querySelector('.vac-message-container .vac-message-card .vac-text-username'); 33 | if (name) { 34 | name.remove(); 35 | } 36 | const lastMessage = document.getElementById(messageUserList[index - 1].id); 37 | if (!lastMessage) { 38 | return; 39 | } 40 | const lastMessageAvatar = lastMessage.querySelector('.el-avatar') as HTMLDivElement; 41 | if (lastMessageAvatar) { 42 | lastMessageAvatar.style.opacity = "0"; 43 | } 44 | // const lastName = lastMessage.querySelector('.vac-message-container .vac-message-card .vac-text-username') 45 | // if (lastName) { 46 | // lastName.remove(); 47 | // } 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /src/functions/fix-message-content-width.ts: -------------------------------------------------------------------------------- 1 | import { IMessageUserList } from "../types"; 2 | 3 | /** 4 | * 修复消息内容宽度 5 | */ 6 | export function fixMessageContentWidth(messageUserList: IMessageUserList) { 7 | messageUserList.forEach((message) => { 8 | const messageElement = document.getElementById(message.id); 9 | if (!messageElement) { 10 | return; 11 | } 12 | const messageContent = messageElement.querySelector( 13 | ".vac-message-container .vac-message-card .vac-message-content" 14 | ); 15 | if (!messageContent) { 16 | return; 17 | } 18 | if (!messageContent.innerHTML) { 19 | messageContent.remove(); 20 | } 21 | const stickers = messageElement.querySelectorAll( 22 | ".vac-message-container .vac-message-card div > div > span" 23 | ) as NodeListOf; 24 | // 这个时候的 stickers 实际上是同时匹配了表情和文字的,他们的区别在于文字的 span 里面还有有一个 span class 叫做 vac-message-content 25 | if (stickers && stickers.length > 1) { 26 | // 如果这个 sticker 里面有多个 span,那么就需要考虑是不是有其他的 sticker 27 | stickers.forEach((sticker, index) => { 28 | const messageContent = sticker.querySelector(".vac-message-content"); // 如果这个 sticker 里面有 vac-message-content,那么就是文字 29 | // 如果这个 sticker 前后还有一个文字,实际上 sticker 不应该被处理 30 | const before = 31 | stickers[index - 1] && 32 | stickers[index - 1].querySelector(".vac-message-content"); 33 | const after = 34 | stickers[index + 1] && 35 | stickers[index + 1].querySelector(".vac-message-content"); 36 | if (before || after) { 37 | return; 38 | } 39 | if (!messageContent) { 40 | sticker.style.marginLeft = "-43px"; 41 | sticker.style.marginRight = "43px"; 42 | } 43 | }); 44 | } 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /styles/modal.scss: -------------------------------------------------------------------------------- 1 | 2 | // Dialog 3 | .el-dialog { 4 | --background: #fff; 5 | border-radius: 15px !important; 6 | box-shadow: none !important; 7 | background: var(--background) !important; 8 | width: 35% !important; 9 | } 10 | 11 | .el-dialog__title { 12 | display: none; 13 | } 14 | 15 | .el-dialog__headerbtn { 16 | left: 14px !important; 17 | right: initial !important; 18 | top: 26px !important; 19 | font-size: 18px !important; 20 | border-radius: 100%; 21 | border: 1px solid #7070ee; 22 | width: 28px; 23 | height: 28px; 24 | cursor: default !important; 25 | } 26 | 27 | .el-dialog__close.el-icon.el-icon-close { 28 | color: #7070ee !important; 29 | } 30 | 31 | .dialog .el-dialog__body, 32 | .el-dialog__header { 33 | --background: transparent; 34 | background-color: var(--background) !important; 35 | } 36 | 37 | .el-dialog__header { 38 | border-radius: 12px 12px 0 0; 39 | padding: 5px 20px 10px !important; 40 | cursor: default !important; 41 | } 42 | 43 | .el-tabs__item { 44 | cursor: default !important; 45 | } 46 | 47 | .el-dialog__body { 48 | border-radius: 0 0 15px 15px; 49 | padding: 5px 20px 30px 20px !important; 50 | } 51 | 52 | .contacts-head-container { 53 | background-color: transparent !important; 54 | margin-bottom: 10px; 55 | margin-left: 35px; 56 | } 57 | 58 | .el-collapse-item__header, 59 | .el-collapse-item__content { 60 | --background: transparent; 61 | background: var(--background) !important; 62 | } 63 | 64 | .contact-entry-left .el-avatar { 65 | --size: 30px; 66 | } 67 | 68 | .contact-entry-entry { 69 | padding: 0 !important; 70 | } 71 | 72 | .contact-entry-name { 73 | font-size: 15px; 74 | } 75 | 76 | .contacts-root { 77 | height: 78vh !important; 78 | } 79 | 80 | .el-tab-pane { 81 | margin-top: 10px; 82 | } 83 | 84 | .contact-entry-root { 85 | background: transparent !important; 86 | } -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | permissions: 12 | write-all 13 | jobs: 14 | 15 | typecheck: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set node 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: latest 23 | 24 | - name: Setup 25 | run: npm i -g @antfu/ni 26 | 27 | - name: Install 28 | run: nci 29 | 30 | - name: Typecheck 31 | run: nr typecheck 32 | 33 | build: 34 | needs: typecheck 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v3 38 | - name: Set node 39 | uses: actions/setup-node@v3 40 | with: 41 | node-version: latest 42 | 43 | - name: Setup 44 | run: npm i -g @antfu/ni 45 | 46 | - name: Install 47 | run: nci 48 | 49 | - name: Build 50 | run: nr build 51 | 52 | - name: Upload 53 | uses: actions/upload-artifact@v2 54 | with: 55 | name: icalingua-theme-telegram.zip 56 | path: dist 57 | - name: Zip 58 | run: zip -r icalingua-theme-telegram.zip dist 59 | - name: Upload a Build Artifact 60 | if: github.ref == 'refs/heads/main' 61 | uses: WebFreak001/deploy-nightly@v1.1.0 62 | env: 63 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 64 | with: 65 | upload_url: https://uploads.github.com/repos/wibus-wee/icalingua-theme-telegram/releases/111083577/assets{?name,label} 66 | release_id: 111083577 67 | asset_path: ./icalingua-theme-telegram.zip 68 | asset_name: icalingua-theme-telegram-$$.zip 69 | asset_content_type: application/zip 70 | max_releases: 7 -------------------------------------------------------------------------------- /styles/textarea.scss: -------------------------------------------------------------------------------- 1 | 2 | .vac-box-footer { 3 | border-top: 1px solid rgb(221, 221, 221) !important; 4 | background-color: var(--tg-panel-background) !important; 5 | padding: 5px 10px !important; 6 | } 7 | 8 | .vac-reply-container { 9 | background-color: var(--tg-panel-background) !important; 10 | margin-bottom: 10px; 11 | padding: 5px 5px 0 10px; 12 | } 13 | 14 | .vac-reply-box { 15 | background-color: var(--tg-panel-background) !important; 16 | border-radius: 0 !important; 17 | border-left: 2px solid rgb(131, 98, 206) !important; 18 | } 19 | 20 | .vac-textarea { 21 | border: none !important; 22 | font-size: 14px !important; 23 | margin-left: 35px !important; 24 | } 25 | 26 | .vac-icon-textarea { 27 | margin-bottom: 10px; 28 | } 29 | 30 | /* file upload */ 31 | .vac-icon-textarea > :nth-child(2) { 32 | position: absolute; 33 | left: 10px; 34 | } 35 | 36 | .vac-textarea::-webkit-input-placeholder { 37 | color: rgb(153, 153, 153) !important; 38 | font-size: 14px; 39 | transition: all 0.2s ease-in-out; 40 | } 41 | 42 | .vac-textarea:focus::-webkit-input-placeholder { 43 | transform: translateX(5px); 44 | } 45 | 46 | #vac-icon-emoji, 47 | #vac-icon-paperclip, 48 | #vac-icon-close-outline { 49 | transition: all 0.2s ease-in-out; 50 | fill: rgb(153, 153, 153) !important; 51 | transform-origin: center center; 52 | } 53 | 54 | #vac-icon-paperclip { 55 | transform: rotate(30deg); 56 | } 57 | 58 | .vac-svg-button { 59 | cursor: default !important; 60 | } 61 | 62 | .vac-svg-button:hover { 63 | transform: none; 64 | opacity: 1; 65 | } 66 | 67 | .vac-svg-button:hover #vac-icon-emoji, 68 | .vac-svg-button:hover #vac-icon-paperclip { 69 | fill: var(--chat-icon-color-emoji) !important; 70 | } 71 | 72 | #vac-icon-send { 73 | transition: all 0.2s ease-in-out; 74 | } 75 | 76 | .vac-svg-button svg { 77 | --size: 25px; 78 | width: var(--size) !important; 79 | height: var(--size) !important; 80 | } -------------------------------------------------------------------------------- /styles/submit-modal.scss: -------------------------------------------------------------------------------- 1 | .el-message-box { 2 | border-radius: 12px !important; 3 | position: relative; 4 | width: 335px !important; 5 | height: 300px !important; 6 | } 7 | 8 | .el-message-box__status { 9 | display: none; 10 | display: inline-flex !important; 11 | flex-direction: column; 12 | align-content: center; 13 | align-items: center; 14 | width: 335px !important; 15 | height: 295px; 16 | } 17 | 18 | .el-message-box__title { 19 | margin-left: -10px; 20 | font-weight: 700; 21 | } 22 | 23 | .el-message-box__headerbtn { 24 | right: initial !important; 25 | font-size: 18px !important; 26 | border-radius: 100%; 27 | border: 1px solid #7070ee; 28 | width: 28px; 29 | height: 28px; 30 | cursor: default !important; 31 | top: 11px; 32 | left: -106px; 33 | } 34 | 35 | .el-message-box__close.el-icon-close { 36 | color: #7070ee !important; 37 | } 38 | .el-message-box__header { 39 | height: 30px; 40 | align-items: center; 41 | } 42 | .el-message-box__content { 43 | height: 100%; 44 | width: 100%; 45 | background: rgb(237 237 244); 46 | // background: #fff; 47 | } 48 | 49 | .el-message-box__btns { 50 | position: absolute; 51 | bottom: 0; 52 | padding: 12px !important; 53 | width: 100%; 54 | justify-content: center; 55 | display: flex; 56 | background: var(--tg-panel-background); 57 | } 58 | 59 | .el-message-box__btns .el-butoon:not(.el-button--primary) { 60 | display: none; 61 | } 62 | 63 | .el-message-box__btns .el-button--primary { 64 | color: #409EFF !important; 65 | border: none !important; 66 | margin: 0 !important; 67 | border: none !important; 68 | background: transparent !important; 69 | font-size: 13px !important; 70 | } 71 | 72 | .el-message-box__container { 73 | background: #fff; 74 | position: absolute; 75 | top: 25%; 76 | left: 11%; 77 | border-radius: 10px; 78 | height: 50%; 79 | width: 80%; 80 | display: flex; 81 | align-items: center; 82 | justify-content: center; 83 | } 84 | 85 | .el-message-box__status+.el-message-box__message { 86 | padding: none; 87 | } 88 | 89 | -------------------------------------------------------------------------------- /styles/stickers-panel.scss: -------------------------------------------------------------------------------- 1 | /* sticker panel */ 2 | .panel-right { 3 | box-shadow: 1px 0 8px 1px #0000001f !important; 4 | transition: all 0.2s ease-in-out; 5 | transform-origin: bottom right; 6 | will-change: scale; 7 | border: none !important; 8 | } 9 | 10 | .bg { 11 | position: relative; 12 | } 13 | 14 | .bg > div:not(.head):not(.stickers_dir):not(.emoji-panel) { 15 | margin-bottom: 55px; 16 | } 17 | 18 | .vac-fade-stickers-enter-active { 19 | transform-origin: bottom right; 20 | will-change: scale; 21 | /* animation: tickers-enter-active 0.2s ease-in-out; */ 22 | animation: scaleIn; 23 | animation-duration: 0.2s; 24 | } 25 | 26 | @keyframes scaleIn { 27 | 0% { 28 | scale: 0.03; 29 | } 30 | 31 | 100% { 32 | scale: 1; 33 | } 34 | } 35 | 36 | .panel-right div.head { 37 | position: absolute; 38 | bottom: 0; 39 | width: 100%; 40 | align-items: center; 41 | justify-content: center; 42 | padding: 0; 43 | z-index: 1; 44 | height: 50px !important; 45 | min-height: 50px !important; 46 | border-top: 1px solid #e1e4e8; 47 | border-bottom: none !important; 48 | } 49 | 50 | .panel-right div.head a { 51 | color: rgb(190, 190, 190) !important; 52 | } 53 | 54 | .opinion { 55 | cursor: default !important; 56 | } 57 | 58 | .title { 59 | width: initial !important; 60 | } 61 | 62 | .panel-right .title { 63 | border-radius: 0 0 10px 10px !important; 64 | } 65 | 66 | .panel-right .title a { 67 | margin: 0 !important; 68 | font-weight: 500 !important; 69 | font-size: 14px; 70 | padding: 5px 10px; 71 | transition: all 0.2s ease-in-out; 72 | border-radius: 20px; 73 | } 74 | 75 | .panel-right .title a.selected { 76 | background-color: rgb(224, 224, 224) !important; 77 | color: rgb(1, 1, 1) !important; 78 | } 79 | 80 | .grid { 81 | position: sticky; 82 | top: 10px; 83 | bottom: 50px; 84 | gap: 7px; 85 | } 86 | 87 | .grid > div img { 88 | border: none !important; 89 | } 90 | 91 | .container-search input { 92 | background: var(--tg-panel-background) !important; 93 | } 94 | 95 | .transition-element { 96 | transition: transform 0.3s; 97 | } 98 | -------------------------------------------------------------------------------- /dev-server.js: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import { readFileSync, readdirSync, watch } from "fs"; 3 | import chalk from "chalk"; 4 | import path from "path"; 5 | import consola from "consola"; 6 | 7 | console.clear() 8 | let isCompiling = false; // 是否正在编译(防止多次编译) 9 | const watcher = ["addon.js", "style.scss", "config.js"] 10 | const cache = {}; 11 | const pwd = execSync("pwd", { encoding: "utf-8" }).trim() 12 | function cacheFiles(dir) { 13 | readdirSync(dir, { withFileTypes: true }).forEach((file) => { 14 | if (file.isDirectory()) { 15 | cacheFiles(`${dir}/${file.name}`) 16 | } else { 17 | // consola.info(`${chalk.cyan("[Cache]")} Caching ${chalk.green(file.name)}...`) 18 | cache[`${dir}/${file.name}`] = readFileSync(`${dir}/${file.name}`, { encoding: "utf-8" }) 19 | } 20 | }) 21 | } 22 | cacheFiles(path.resolve(pwd, "./src")) 23 | watcher.forEach((file) => { 24 | // consola.info(`${chalk.cyan("[Cache]")} Caching ${chalk.green(file)}...`) 25 | cache[`${path.resolve(pwd)}/${file}`] = readFileSync(`${file}`, { encoding: "utf-8" }) 26 | }) 27 | 28 | consola.info(`${chalk.cyan("[Cache]")} Caching complete\n`); 29 | 30 | watch("./", { recursive: true }, (eventType, filename) => { 31 | if (watcher.includes(filename) || filename.includes("src")) { 32 | // 比对文件内容是否发生变化 33 | if (cache[`${path.resolve(pwd)}/${filename}`] === readFileSync(`${path.resolve(pwd)}/${filename}`, { encoding: "utf-8" })) { 34 | return 35 | } 36 | // 更新缓存 37 | cache[filename] = readFileSync(filename); 38 | consola.info(`${chalk.cyan("[Cache]")} ${chalk.green(filename)} updated`) 39 | // consola.info(`${filename} changed, recompiling...`) 40 | if (isCompiling) { 41 | consola.info(`${chalk.bold(chalk.blue("[Dev]"))} Recompilation in progress, refusing to recompile`) 42 | return; 43 | } 44 | consola.info(`${chalk.bold(chalk.blue("[Dev]"))} Recompiling...`) 45 | isCompiling = true 46 | execSync("sh replace.sh", { stdio: "ignore" }); 47 | isCompiling = false 48 | // consola.info("Recompilation complete") 49 | } 50 | }) 51 | consola.info(`${chalk.bold(chalk.blue("[Dev]"))} Watching for file changes...`) 52 | consola.info(`${chalk.bold(chalk.blue("[Dev]"))} Press ${chalk.green("Ctrl + C")} to exit`) 53 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export function injectScript( 2 | url: string, 3 | async: boolean, 4 | callback: ((this: GlobalEventHandlers, ev: Event) => any) | null 5 | ) { 6 | const script = document.createElement("script"); 7 | script.src = url; 8 | script.async = async; 9 | document.head.appendChild(script); 10 | if (callback) { 11 | script.onload = callback; 12 | } 13 | } 14 | 15 | export function injectCSS(url: string) { 16 | const link = document.createElement("link"); 17 | link.rel = "stylesheet"; 18 | link.href = url; 19 | document.head.appendChild(link); 20 | } 21 | 22 | export function setRootStyle(key: any, value: any) { 23 | const style = document.getElementById("custom-root-style"); 24 | if (!style) { 25 | return; 26 | } 27 | style.innerHTML = style.innerHTML.replace( 28 | new RegExp(`--${key}:.*;`), 29 | `--${key}: ${value};` 30 | ); 31 | createConsole("ADDON", `set root style ${key} to ${value}`); 32 | } 33 | 34 | export function getRootStyle(key: any) { 35 | const style = document.getElementById("custom-root-style"); 36 | if (!style) { 37 | return null; 38 | } 39 | const reg = new RegExp(`--${key}: (.*);`); 40 | const match = style.innerHTML.match(reg); 41 | if (match) { 42 | return match[1]; 43 | } 44 | return null; 45 | } 46 | 47 | export function getRoomsPanelWidth() { 48 | return (document.getElementsByClassName("rooms-panel")[0] as HTMLElement || undefined) 49 | ?.style.width; 50 | } 51 | 52 | export function createConsole(code: string, desc: string) { 53 | if (!window.theme.log) return; 54 | console.log( 55 | `%c ${code} %c ${desc}`, 56 | "background: #eaeffd;color:#335eea;padding: 4px 6px;border-radius:3px;", 57 | "background:unset;color:unset;" 58 | ); 59 | } 60 | 61 | export function createConsoleGroup( 62 | code: string, 63 | desc: string, 64 | group: { 65 | text: string; 66 | type: "log" | "error" | "warn" | "info"; 67 | }[] 68 | ) { 69 | if (!window.theme.log) return; 70 | console.groupCollapsed( 71 | `%c ${code} %c ${desc}`, 72 | "background: #eaeffd;color:#335eea;padding: 4px 6px;border-radius:3px;", 73 | "background:unset;color:unset;" 74 | ); 75 | group.forEach((item) => { 76 | console[item.type](item.text); 77 | }); 78 | console.groupEnd(); 79 | } 80 | -------------------------------------------------------------------------------- /src/functions/special-username-color.ts: -------------------------------------------------------------------------------- 1 | import { IMessageUserList } from "../types"; 2 | 3 | /** 4 | * special username color 5 | */ 6 | export function specialUsernameColor(messageUserList: IMessageUserList) { 7 | messageUserList.forEach((message) => { 8 | const messageElement = document.getElementById(message.id); 9 | if (!messageElement) { 10 | return; 11 | } 12 | const messageUsername = messageElement.querySelector('.vac-message-container .vac-message-card .vac-text-username'); 13 | if (!messageUsername) { 14 | return; 15 | } 16 | const color = calculateColor(message.name); 17 | const messageUsernameSpan = messageUsername.querySelector('span:first-child') as HTMLSpanElement; 18 | if (messageUsernameSpan) { 19 | messageUsernameSpan.style.color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`; 20 | } 21 | }) 22 | } 23 | 24 | function calculateColor(qqID: string) { 25 | const number = Number(qqID.toString().replace(/\D/g, '')); 26 | 27 | // 进行一系列复杂的计算操作 28 | const calculationResult1 = performCalculation(number, 1); 29 | const calculationResult2 = performCalculation(number, 2); 30 | const calculationResult3 = performCalculation(number, 3); 31 | 32 | // 将计算结果映射到0到255的范围 33 | const mappedValue1 = mapToRange(calculationResult1, 0, 1000, 30, 205); 34 | const mappedValue2 = mapToRange(calculationResult2, 0, 1000, 30, 205); 35 | const mappedValue3 = mapToRange(calculationResult3, 0, 1000, 30, 205); 36 | 37 | // 将映射后的值分配给RGB颜色空间的相应分量 38 | const redComponent = Math.abs(mappedValue1 + 80); 39 | const greenComponent = Math.abs(mappedValue2 + 80); 40 | const blueComponent = Math.abs(mappedValue3 + 80); 41 | 42 | // 返回RGB颜色值 43 | return [redComponent, greenComponent, blueComponent]; 44 | } 45 | 46 | function performCalculation(number: number, operation: number) { 47 | // 在这里执行更复杂的计算操作 48 | if (operation === 1) { 49 | return Math.sin(number) * 500; 50 | } else if (operation === 2) { 51 | return Math.cos(number) * 300; 52 | } else if (operation === 3) { 53 | return Math.tan(number) * 200; 54 | } else { 55 | return 0; 56 | } 57 | } 58 | 59 | function mapToRange(value: number, inMin: number, inMax: number, outMin: number, outMax: number) { 60 | // 将值从输入范围映射到输出范围 61 | return Math.abs(((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin); 62 | } -------------------------------------------------------------------------------- /src/core/modify-chat-box.ts: -------------------------------------------------------------------------------- 1 | import { Functions } from "../functions"; 2 | import { IMessageUserList } from "../types"; 3 | import { createConsoleGroup } from "../utils"; 4 | 5 | /** 6 | * 修改聊天框,对聊天框进行深度定制 7 | */ 8 | export function modifyChatBox() { 9 | // 在同一用户连续发送的消息中,只保留最后一个头像 10 | const messageList = document.querySelectorAll(".vac-message-box"); 11 | // 如果这个是“我”发的消息,在 vac-message-box 上会多一个 vac-offset-current 的 class 12 | // 接着我们需要构造一个列表,列表内储存着发消息的用户、信息 ID。 13 | // 其中如果是我本人发的消息,信息 ID 为 0 & 用户为 'current' 14 | const messageUserList = [] as IMessageUserList; 15 | messageList.forEach((message) => { 16 | // id 就在 message 的 attribute 里面 17 | const id = message.getAttribute("id")!; 18 | // 判断是否是“我”发的消息 19 | const isCurrent = message.classList.contains("vac-offset-current"); 20 | if (isCurrent) { 21 | messageUserList.push({ 22 | id, 23 | name: "current", 24 | }); 25 | } 26 | // 如果不是“我”发的消息,那么我们需要获取发消息的用户 & 信息 ID 27 | else { 28 | // 用户在 vac-message-box > vac-message-sender-avatar > el-avatar > img 的 src 里面. 29 | const imgSrc = message.querySelector(".el-avatar img"); 30 | if (!imgSrc) { 31 | // 在私聊模式下,如果对方没有头像,那么 imgSrc 会是 null 32 | // 这个时候我们就需要把这个消息的 name 都设置为 'friend' 33 | messageUserList.push({ 34 | id, 35 | name: "friend", 36 | }); 37 | return; 38 | } 39 | // 由于 imgSrc 是一个 url,我们需要从 url 里面提取出用户 (nk=xxx) 40 | const name = imgSrc.getAttribute("src")!.match(/nk=(.*)/)![1]; 41 | messageUserList.push({ 42 | id, 43 | name, 44 | }); 45 | } 46 | }); 47 | Promise.all( 48 | window.theme.chatbox.map((fn) => { 49 | return Functions[fn](messageUserList); 50 | }) 51 | ); 52 | } 53 | 54 | export function modifyChatBoxInterval() { 55 | createConsoleGroup( 56 | "ModifyChatBox", 57 | "These functions will be called:", 58 | window.theme.chatbox.map((f) => { 59 | // 将短横线转换为驼峰 60 | const text = f.replace(/-(\w)/g, function (_all, letter) { 61 | return letter.toUpperCase(); 62 | }) as keyof typeof Functions; 63 | const isFunctionExist = Functions[f] !== undefined; 64 | if (!isFunctionExist) { 65 | window.theme.chatbox = window.theme.chatbox.filter((item) => item !== f); 66 | } 67 | return { 68 | text: `${text} => ${isFunctionExist ? "✅" : "❌"}`, 69 | type: isFunctionExist ? "log" : "warn", 70 | }; 71 | }) 72 | ); 73 | setInterval(() => { 74 | modifyChatBox(); 75 | }, 500); 76 | } 77 | -------------------------------------------------------------------------------- /src/functions/better-image-display.ts: -------------------------------------------------------------------------------- 1 | import { IMessageUserList } from "../types"; 2 | 3 | /** 4 | * Better image display 5 | */ 6 | export function betterImageDisplay(messageUserList: IMessageUserList) { 7 | messageUserList.forEach((message) => { 8 | // 如果这个消息是图片消息,那么我们就需要重新制作一个容器,然后把图片放进去 9 | const messageElement = document.getElementById(message.id); 10 | if (!messageElement) { 11 | return; 12 | } 13 | // 判断下生成了没有 14 | if ( 15 | messageElement 16 | .querySelector(".vac-message-card") 17 | ?.querySelector(".vac-image-tg-container") 18 | ) { 19 | return; 20 | } 21 | const messageImage = messageElement.querySelector( 22 | ".vac-message-container .vac-message-card .vac-image-container" 23 | ); 24 | if (!messageImage || !messageImage.querySelector("img")) { 25 | return; 26 | } 27 | const messageImageSrc = messageImage 28 | .querySelector("img") 29 | ?.getAttribute("src"); 30 | // 这里会有一个问题,我们需要检测一下 messageElement 还有没有 vac-message-content 这个 class,因为它是一个文本消息 31 | // 如果是文本消息,那么我们就不需要重新制作一个容器了 32 | const isTextMessage = messageElement.querySelector( 33 | ".vac-message-container .vac-message-card .vac-message-content" 34 | ); 35 | if (isTextMessage) { 36 | return; 37 | } 38 | // 接着我们就可以开始制作一个容器了 39 | // 先获取一些必要的信息吧 40 | const timestamp = messageElement 41 | .querySelector( 42 | ".vac-message-container .vac-message-card .vac-text-timestamp" 43 | ) 44 | ?.querySelector("span")?.innerText!; 45 | const current = message.name === "current"; 46 | const newMessageImageContainer = document.createElement("div"); 47 | newMessageImageContainer.classList.add("vac-image-tg-container"); 48 | if (current) { 49 | newMessageImageContainer.classList.add("vac-image-tg-container--current"); 50 | } 51 | newMessageImageContainer.innerHTML = ` 52 |
53 | 54 |
55 |
56 | ${timestamp.split(":").slice(0, 2).join(":")} 57 |
58 | `; 59 | // Thanks to @dmlgzs for this fix: https://github.com/wibus-wee/icalingua-theme-telegram/issues/21#issuecomment-1621200630 60 | newMessageImageContainer.onclick = () => { 61 | // 检查一下有没有 vac-forward-container 62 | const forwardContainer = document.querySelector(".vac-forward-container"); 63 | if (!forwardContainer) { 64 | require("electron").ipcRenderer.send("openImage", messageImageSrc, window.theme.localImageViewerByDefault); 65 | } 66 | }; 67 | const messageCard = messageElement.querySelector( 68 | ".vac-message-container .vac-message-card" 69 | ) as HTMLElement; 70 | // 把里面所有内容 hidden 掉 71 | Array.from(messageCard.querySelectorAll("*")).forEach((element) => { 72 | (element as HTMLElement).style.display = "none"; 73 | }); 74 | // 然后把新的图片容器添加进去 75 | messageCard.appendChild(newMessageImageContainer); // 添加新的图片容器 76 | // 并且要设置多一个 class 以便于我们在 CSS 里面控制不显示垃圾边框 77 | messageElement 78 | .querySelector(".vac-message-container") 79 | ?.classList.add("vac-message-container--image"); 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /styles/darkmode.scss: -------------------------------------------------------------------------------- 1 | /* ===== Dark mode ===== */ 2 | html[theme="dark"] { 3 | body { 4 | --tg-panel-background: rgb(34, 34, 34); 5 | --tg-border-color: rgb(68, 68, 68); 6 | --tg-chatbox-background: rgb(47 47 47); 7 | } 8 | 9 | .head, 10 | .vac-room-header, 11 | .root { 12 | background-color: var(--tg-panel-background) !important; 13 | } 14 | 15 | .root { 16 | border-right: 0.3px solid var(--tg-border-color) !important; 17 | } 18 | 19 | .root::after { 20 | content: ""; 21 | position: absolute; 22 | bottom: 0; 23 | right: 0; 24 | width: 78%; 25 | height: 1px; 26 | z-index: 1; 27 | background-color: var(--tg-border-color); 28 | } 29 | 30 | .el-aside { 31 | border-width: 0.3px !important; 32 | border-color: var(--tg-border-color) !important; 33 | } 34 | 35 | .chat-group .selected { 36 | --selected-color: rgb(11 96 254) !important; 37 | } 38 | 39 | .root .selected { 40 | background-color: rgb(6 74 192) !important; 41 | } 42 | 43 | .root:hover .selected { 44 | background-color: rgb(6 74 192) !important; 45 | } 46 | 47 | .vac-box-footer { 48 | border-color: var(--tg-border-color) !important; 49 | } 50 | 51 | .el-badge__content--info { 52 | background-color: rgb(177 201 227) !important; 53 | } 54 | 55 | .el-badge__content { 56 | color: rgb(1 1 1) !important; 57 | border-color: transparent !important; 58 | } 59 | 60 | .vac-container-scroll { 61 | background-color: rgb(27 27 27) !important; 62 | } 63 | 64 | .vac-message-card { 65 | border: 0.5px solid rgb(68, 68, 68) !important; 66 | } 67 | 68 | .vac-message-image-mod { 69 | background-color: var(--tg-chatbox-background) !important; 70 | } 71 | 72 | .vac-message-card.vac-message-current { 73 | border: none; 74 | background-color: rgb(32 93 206) !important; 75 | color: rgb(254 254 254) !important; 76 | } 77 | 78 | .el-input__inner { 79 | background-color: var(--tg-chatbox-background) !important; 80 | } 81 | 82 | .panel-right, 83 | .bg, 84 | .grid, 85 | .grid img, 86 | .grid > div, 87 | .panel-right .title, 88 | .panel-right .head { 89 | background-color: var(--tg-chatbox-background) !important; 90 | } 91 | 92 | .panel-right .title a.selected { 93 | background-color: rgb(65, 65, 65) !important; 94 | color: rgb(224, 224, 224) !important; 95 | } 96 | 97 | .vac-text-username > span:first-child { 98 | color: rgb(48 211 58); 99 | } 100 | 101 | .vac-reply-username { 102 | color: rgb(254, 254, 254) !important; 103 | } 104 | 105 | .vac-reply-message { 106 | border-color: rgb(254, 254, 254) !important; 107 | } 108 | 109 | .vac-message-selected { 110 | --chat-message-bg-color-selected: #494949 !important; 111 | } 112 | 113 | .vac-message-current.vac-message-selected { 114 | background-color: rgb(97, 151, 206) !important; 115 | } 116 | 117 | .vac-message-container--image .vac-message-card { 118 | border: none !important; 119 | background: transparent !important; 120 | } 121 | 122 | .panel-right div.head { 123 | border-color: var(--tg-border-color) !important; 124 | } 125 | 126 | .el-dialog { 127 | --background: #0000009e; 128 | } 129 | 130 | .dialog .el-dialog__body, 131 | .el-dialog__header { 132 | --background: #000; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /styles/room-group.scss: -------------------------------------------------------------------------------- 1 | 2 | .el-aside { 3 | background-color: var(--tg-panel-background) !important; 4 | color: rgb(162, 162, 162) !important; 5 | display: block !important; 6 | position: absolute; 7 | top: 50px; 8 | left: 0; 9 | width: var(--rooms-panel-width) !important; 10 | z-index: 4 !important; 11 | padding: 0 !important; 12 | border-bottom: 1px solid rgb(221, 221, 221) !important; 13 | border-right: 1px solid rgb(221, 221, 221) !important; 14 | } 15 | 16 | @media (max-width: 720px) { 17 | .el-aside { 18 | width: 100% !important; 19 | } 20 | } 21 | 22 | .el-aside .head { 23 | display: none; 24 | } 25 | 26 | .chat-group { 27 | display: flex; 28 | flex-direction: row; 29 | } 30 | 31 | .chat-group a { 32 | width: 100%; 33 | } 34 | 35 | .chat-group a > div { 36 | padding-bottom: 9px; 37 | margin: 0 !important; 38 | font-size: 14px !important; 39 | font-weight: 400 !important; 40 | } 41 | 42 | .chat-group a:hover { 43 | cursor: default !important; 44 | } 45 | 46 | .chat-group a > div > i { 47 | display: none !important; 48 | } 49 | 50 | .chat-group .selected { 51 | position: relative; 52 | --selected-color: rgb(70 135 210); 53 | color: var(--selected-color) !important; 54 | } 55 | 56 | .chat-group .selected::after { 57 | content: ""; 58 | position: absolute; 59 | bottom: 0; 60 | left: 9px; 61 | width: 80%; 62 | height: 3.3px; 63 | border-radius: 2px 2px 0 0; 64 | background-color: var(--selected-color); 65 | animation: selected 0.2s ease-in-out; 66 | } 67 | 68 | @keyframes selected { 69 | 0% { 70 | transform: scale(0.9); 71 | } 72 | 73 | 100% { 74 | transform: scale(1); 75 | } 76 | } 77 | 78 | .rooms-panel { 79 | border: none !important; 80 | } 81 | 82 | .rooms-panel * { 83 | cursor: default !important; 84 | } 85 | 86 | .rooms-panel .content { 87 | margin-top: 36px; 88 | } 89 | 90 | .el-input__inner { 91 | background-color: var(--tg-chatbox-background) !important; 92 | border: none !important; 93 | border-radius: 5px !important; 94 | height: 37px; 95 | text-transform: none !important; 96 | } 97 | 98 | .el-input__inner::-webkit-input-placeholder { 99 | color: rgb(153, 153, 153) !important; 100 | text-align: center; 101 | font-weight: 400; 102 | transition: all 0.2s ease-in-out; 103 | } 104 | 105 | .el-input__prefix, 106 | .el-input__suffix { 107 | top: -1px; 108 | animation: input-prefix-suffix 0.2s ease-in-out; 109 | } 110 | 111 | @keyframes input-prefix-suffix { 112 | 0% { 113 | opacity: 0; 114 | } 115 | 116 | 100% { 117 | opacity: 1; 118 | } 119 | } 120 | 121 | .el-icon-user, 122 | .el-icon-delete { 123 | color: rgb(112 170 238); 124 | font-weight: 500 !important; 125 | } 126 | 127 | /* .el-input__inner 在输入的时候应该全部丢到左边 */ 128 | .el-input__inner:focus.el-input__inner::-webkit-input-placeholder { 129 | transform: translateX(-62px); 130 | } 131 | 132 | .root { 133 | transition: none !important; 134 | } 135 | 136 | .root:hover { 137 | background-color: var(--tg-panel-background) !important; 138 | } 139 | 140 | .root .selected { 141 | background-color: rgb(87 146 204) !important; 142 | } 143 | 144 | .root:hover .selected { 145 | background-color: rgb(87 146 204) !important; 146 | } 147 | 148 | .root .selected .entry .right * { 149 | color: rgb(254, 254, 254) !important; 150 | } 151 | 152 | .root .entry .right .flex.l1 .name { 153 | font-weight: 500; 154 | font-size: 15px; 155 | } 156 | 157 | .root .selected .entry .right .flex.l1 .name { 158 | font-weight: 400; 159 | } 160 | 161 | .root .entry .right .flex .desc { 162 | font-size: 12px; 163 | color: rgb(160, 160, 160); 164 | } 165 | 166 | .el-badge__content--info { 167 | background-color: rgb(215, 215, 215) !important; 168 | } 169 | 170 | .el-avatar { 171 | --size: 48px; 172 | width: var(--size) !important; 173 | height: var(--size) !important; 174 | } 175 | 176 | .sub-avatar { 177 | --size: 30px !important; 178 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 | 6 | # Telegram Theme For Icalingua++ 7 | 8 | 一个基于 [Icalingua++](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus) 的 Telegram 风格主题。 9 | 10 | ## Motivation | 动机 11 | 12 | 我非常喜欢 Telegram Desktop 的 UI,但是在很多时候我都没法访问 Telegram,并且地区使用习惯的原因,我很难使用 Telegram,而是使用 QQ。 13 | 14 | 但是 Tencent QQ NT 版本的 UI 完全没有办法自由定制,即使定制成功了也是 HACK 进去的,对这款软件来说它并不合法。所以我决定使用 Icalingua++ 来实现这个主题。它完全可以实现 Telegram 的 UI,而且它是开源的,可以让更多的人使用。 15 | 16 | > 总结:**QQ NT 一坨屎,Icalingua++ 大大滴好!** 17 | 18 | 但其实这个主题与其名曰主题,不如说是一个增强版的 Icalingua++,因为它不仅仅是一个主题,它还会增强 Icalingua++ 的功能与体验。 19 | 20 | ## Attentions | 注意事项 21 | 22 | - 由于 Icalingua++ 的限制,我改变了消息列表的 DOM 结构,所以我暂时无法实现点击图片放大的功能。后续可能我会尝试重写灯箱来实现这个功能。[Issue #16](https://github.com/wibus-wee/icalingua-theme-telegram/issues/16) 23 | - 它**强制改变**了很多原本的**DOM结构**,这可能会导致一些功能出现问题,如果你发现了这些问题,欢迎提交 [Issue](https://github.com/wibus-wee/icalingua-theme-telegram/issues)。 24 | - 由于我们想要增强聊天功能,我们可能需要另外启动一个子进程来处理一些信息。如果你**不信任我 / 不信任仓库代码**,你可以不使用这个主题。 25 | - 在 [#32](https://github.com/wibus-wee/icalingua-theme-telegram/pull/32) 中,我实现了手动控制功能启动功能,你现在可以在 `config.js` 中设置你想要启动的功能了,你也可以将 `manual` 设置为 `false` 来启动所有功能。有关更多配置信息,请参阅 [Config | 配置](#config--配置)。 26 | 27 | ## Features | 特性 28 | 29 | - **基础样式。** 将 Telegram 的大部分样式移植到 Icalingua++。 30 | - **深度修改。** 将同一联系人的多条消息合并为一条,以减少界面占用。 31 | - **更好的图片信息显示效果。** 以更好的方式显示图片信息。 32 | - **新图标。** 用 Telegram 风格的图标替换图标。 33 | - **漂亮的模态框。** 更改模态框的样式,使其更加美观。 34 | - **不同的用户名颜色。** 为每个用户名分配不同的颜色,以便更好地区分不同的联系人。 35 | - **良好的动效。** 为 Icalingua++ 移除与主题不和谐的动效以及添加更多合理的动效。 36 | - **更多样式。** 将添加更多样式,使 Icalingua++ 更像 Telegram。 37 | - **更好的开发体验。** 自动重载样式和页面,以便开发者更好地开发主题。 38 | 39 | ## Installation | 安装 40 | 41 | ### Automatic | 自动安装 42 | 43 | 1. 下载最新的发布版本或从 [CI Release](https://github.com/wibus-wee/icalingua-theme-telegram/releases) 下载最新的构建版本。 44 | 2. 给予执行权限 `chmod +x install.sh` 45 | 3. 在解压缩包后的目录下运行 `./install.sh`。 46 | 4. 重启 Icalingua++。 47 | 48 | ### Manual | 手动安装 49 | 50 | #### 从 CI 下载 51 | 52 | 1. 下载最新的发布版本或从 [CI Release](https://github.com/wibus-wee/icalingua-theme-telegram/releases) 下载最新的构建版本。 53 | 2. 将 `addon.js`, `style.css`, `main.js`, `config.js` 复制到 Icalingua++ 的[数据目录](https://github.com/Icalingua-plus-plus/Icalingua-plus-plus#%E9%BB%98%E8%AE%A4%E6%95%B0%E6%8D%AE%E7%9B%AE%E5%BD%95) 54 | 3. 重启 Icalingua++。 55 | 56 | #### 从源码安装 57 | 58 | 1. 克隆这个仓库。 59 | 2. 安装依赖 `pnpm install`。 60 | 3. 给予执行权限 `chmod +x dist/install.sh`。 61 | 4. 运行 `cd dist && ./install.sh`。 62 | 5. 重启 Icalingua++。 63 | 64 | ## Enhancements & Feat. | 增强 & 新功能 65 | 66 | 这个文件用于帮助一些由于 DOM 结构的原因无法直接通过改变 CSS 实现目标样式的元素。已经实现的功能有: 67 | 68 | - [x] 获取 ChatGroup 的宽度以改变 ChatGroup Aside 为 Telegram 风格的头部菜单栏。 69 | - [x] 合并同一联系人的多条消息为一条。 70 | - [x] 更好的图片信息显示效果。(仅针对单张图片消息) 71 | - [x] 移除回复消息的图标并改为点击即可回复消息的样式。 72 | - [x] 为每个用户名分配不同的颜色。 73 | - [x] 自动重载 CSS 和 JS 文件。 74 | - [x] 手动控制功能启动功能。 75 | - [ ] 全新的图像显示器。 76 | - [ ] 主题自动更新器。 77 | - [ ] 用 Telegram 风格的图标替换图标。 78 | - [ ] 更改模态框的样式,使其更加美观。 79 | - [ ] 鼠标滑动以回复消息 80 | 81 | ## Config | 配置 82 | 83 | 在 [#32](https://github.com/wibus-wee/icalingua-theme-telegram/pull/32) 中,我们引入了一个新的配置文件 `config.js`,你可以在这个文件中配置你想要启动的功能。有关这个文件的配置项定义,你可以前往 [types.d.ts](./types.d.ts) 查看。在此我简单介绍一下配置项: 84 | 85 | > **Note** 86 | > 87 | > **不知道咋写的先学下 JavaScript 吧,或者将 `manual` 设置为 false,这样所有功能都会启动。** 88 | 89 | ### core -- 启动的核心功能 90 | 91 | 你可以去前往 [core](./src/core//index.ts) 查看所有的核心功能。你需要填入的是核心功能的 Key。如: 92 | 93 | 在文件里有一行代码: 94 | 95 | ```ts 96 | "modify-chat-box-interval": modifyChatBoxInterval, 97 | ``` 98 | 99 | 我想启动这个功能,那么你在 config.js 的 core 中需要填入的是 [`modify-chat-box-interval`],以此类推。 100 | 101 | ### chatbox --- 启动的聊天框修改功能 102 | 103 | 与 [core](#core----启动的核心功能) 类似,你可以去前往 [chatbox](./src/functions/index.ts) 查看所有的聊天框修改功能。你需要填入的是聊天框修改功能的 Key。 104 | 105 | ### 其他注意事项 106 | 107 | - 你可以在 `config.js` 中设置 `manual` 为 `false` 来启动所有功能。 108 | - 当 `dev` 为 `true` 且你启动了 `fileChangesListener` 时,当你对 JS 文件进行修改时,Icalingua++ 会自动重载窗口,对 CSS 文件修改时,Icalingua++ 会自动重载 CSS 文件。 109 | - 当 `dev` 为 `true` 时,全部功能都会启动,你无法通过 `manual` 或其他办法来关闭功能(除了删代码 🙂)。 110 | - 修改了 `config.js` 后,你需要重启 / 重载 Icalingua++ 才能使配置生效。 111 | - 你如果“不小心”填错了功能的 Key,你大可以放心这个功能是不会被启动的,并且在控制台会有错误警告。 112 | - 如果你对这种**控制台乱拉屎**的行为非常厌恶 🤬,你可以将 `log` 设置为 `false` 来关闭控制台输出。 113 | 114 | ## Preview 115 | 116 | |Light|Dark| 117 | |---|---| 118 | |light|dark| 119 | 120 | ## Author 121 | 122 | Telegram Theme For Icalingua++ © Wibus, Released under AGPLv3. Created on Jul 1, 2023 123 | 124 | > [Personal Website](http://wibus.ren/) · [Blog](https://blog.wibus.ren/) · GitHub [@wibus-wee](https://github.com/wibus-wee/) · Telegram [@wibus✪](https://t.me/wibus_wee) 125 | -------------------------------------------------------------------------------- /styles/chatbox.scss: -------------------------------------------------------------------------------- 1 | 2 | .vac-col-messages { 3 | background-color: var(--tg-panel-background) !important; 4 | } 5 | 6 | .vac-room-name { 7 | font-weight: 500; 8 | font-size: 15px; 9 | } 10 | 11 | .vac-room-info { 12 | font-size: 12px; 13 | } 14 | 15 | .vac-room-options { 16 | color: rgb(67, 128, 198); 17 | font-size: 20px; 18 | } 19 | 20 | .vac-room-options:hover { 21 | transform: none; 22 | opacity: 1; 23 | } 24 | 25 | .vac-container-scroll { 26 | background-color: var(--tg-panel-background) !important; 27 | padding-left: 10px; 28 | padding-right: 10px; 29 | } 30 | 31 | div[message-actions] { 32 | border-radius: 10px !important; 33 | } 34 | 35 | .vac-message-sender-avatar .el-avatar { 36 | --size: 32px; 37 | width: var(--size) !important; 38 | height: var(--size) !important; 39 | } 40 | 41 | .vac-message-card { 42 | background-color: var(--tg-chatbox-background) !important; 43 | box-shadow: none !important; 44 | border-radius: 20px !important; 45 | padding: 7px 14px 7px 14px !important; 46 | position: relative; 47 | } 48 | 49 | .vac-message-card > .vac-reply-message { 50 | margin-left: 1px; 51 | margin-top: 3px; 52 | } 53 | 54 | .vac-message-card a { 55 | font-weight: 400; 56 | } 57 | 58 | .vac-message-card.vac-message-current { 59 | padding: 6px 58px 6px 14px !important; 60 | } 61 | 62 | .vac-message-selected { 63 | --chat-message-bg-color-selected: rgb(255 230 230) !important; 64 | } 65 | 66 | html[theme="light"] .vac-message-current.vac-message-selected { 67 | background-color: rgb(32, 93, 206) !important; 68 | } 69 | 70 | .vac-message-container--image 71 | .vac-message-selected 72 | .vac-image-tg-container 73 | .vac-image-tg-container__image 74 | img { 75 | // 亮 & 提高对比度 76 | filter: brightness(1.5) contrast(1.5); 77 | transition: all 0.2s ease-in-out; 78 | } 79 | 80 | .vac-text-username { 81 | display: block !important; 82 | } 83 | 84 | .vac-text-username > span:first-child { 85 | color: rgb(131, 98, 206); 86 | font-weight: 400 !important; 87 | } 88 | 89 | .vac-message-card.vac-message-current { 90 | background: rgb(97 151 206) !important; 91 | color: rgb(254, 254, 254) !important; 92 | } 93 | 94 | .vac-message-current .vac-text-timestamp { 95 | color: rgb(254, 254, 254) !important; 96 | } 97 | 98 | .vac-message-content { 99 | font-size: 13px; 100 | } 101 | 102 | .vac-message-card > div:not(.vac-text-timestamp) { 103 | margin-right: 43px !important; 104 | } 105 | 106 | .vac-message-current > div:not(.vac-text-timestamp) { 107 | margin-right: 0 !important; 108 | } 109 | 110 | .vac-message-current > div > div > a { 111 | color: rgb(254, 254, 254) !important; 112 | text-decoration: underline !important; 113 | } 114 | 115 | .vac-card-date { 116 | background-color: var(--tg-panel-background) !important; 117 | box-shadow: none !important; 118 | color: rgb(160, 160, 160) !important; 119 | } 120 | 121 | .vac-message-image-mod { 122 | background-color: var(--tg-panel-background) !important; 123 | } 124 | 125 | .vac-text-timestamp { 126 | position: absolute; 127 | right: 10px; 128 | bottom: 5px; 129 | font-style: italic !important; 130 | opacity: 0.7 !important; 131 | } 132 | 133 | .vac-icon-scroll { 134 | box-shadow: 0 1px 8px 1px #0000001f !important; 135 | } 136 | 137 | .vac-icon-scroll svg path { 138 | fill: #707579 !important; 139 | } 140 | 141 | .vac-reply-message { 142 | border-radius: 0 !important; 143 | background-color: inherit !important; 144 | border-left: 2px solid rgb(131, 98, 206) !important; 145 | padding: 2px 10px !important; 146 | color: inherit !important; 147 | position: relative; 148 | } 149 | 150 | .vac-reply-username { 151 | color: rgb(131, 98, 206) !important; 152 | font-weight: 400 !important; 153 | } 154 | 155 | .vac-message-current .vac-reply-username, 156 | .vac-message-current .vac-reply-message { 157 | color: rgb(254, 254, 254) !important; 158 | border-color: rgb(254, 254, 254) !important; 159 | } 160 | 161 | 162 | .vac-card-window > pre { 163 | background-color: var(--tg-panel-background) !important; 164 | border-radius: 13px !important; 165 | padding: 15px !important; 166 | margin: 10px 0 !important; 167 | font-size: 12px !important; 168 | } 169 | 170 | .vac-image-tg-container { 171 | position: relative; 172 | } 173 | 174 | .vac-image-tg-container__image img { 175 | border-radius: 13px; 176 | max-height: 300px; 177 | max-width: 300px; 178 | } 179 | 180 | .vac-image-tg-container__image img { 181 | border: 0.5px solid var(--tg-chatbox-background); 182 | } 183 | 184 | .vac-image-tg-container--current img { 185 | border-color: rgb(97 151 206); 186 | } 187 | 188 | .vac-image-tg-container__timestamp { 189 | position: absolute; 190 | right: -5px; 191 | bottom: 15px; 192 | font-style: italic; 193 | opacity: 0.7 !important; 194 | color: rgb(254, 254, 254); 195 | backdrop-filter: blur(5px) brightness(0.65); 196 | border-radius: 50px; 197 | font-size: 10px; 198 | padding: 2px 7px; 199 | letter-spacing: 0.5px; 200 | } 201 | 202 | .vac-message-container--image .vac-message-card { 203 | border: none; 204 | padding: 0 !important; 205 | background: transparent !important; 206 | } 207 | 208 | .vac-reply-username > div { 209 | width: 100% !important; 210 | position: absolute; 211 | height: 100% !important; 212 | cursor: default !important; 213 | opacity: 0; 214 | top: 0; 215 | bottom: 0; 216 | } 217 | 218 | .vac-reply-username > div svg { 219 | stroke: #707579 !important; 220 | } 221 | 222 | .vac-reply-content { 223 | color: inherit !important; 224 | } 225 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | devDependencies: 4 | '@rollup/plugin-node-resolve': 5 | specifier: ^15.1.0 6 | version: 15.1.0(rollup@3.26.1) 7 | '@types/node': 8 | specifier: ^20.3.3 9 | version: 20.3.3 10 | chalk: 11 | specifier: ^5.3.0 12 | version: 5.3.0 13 | consola: 14 | specifier: ^3.2.2 15 | version: 3.2.2 16 | rollup: 17 | specifier: ^3.26.1 18 | version: 3.26.1 19 | rollup-plugin-esbuild: 20 | specifier: ^5.0.0 21 | version: 5.0.0(esbuild@0.18.11)(rollup@3.26.1) 22 | rollup-plugin-ts: 23 | specifier: ^3.2.0 24 | version: 3.2.0(rollup@3.26.1)(typescript@5.1.6) 25 | sass: 26 | specifier: ^1.63.6 27 | version: 1.63.6 28 | typescript: 29 | specifier: ^5.1.6 30 | version: 5.1.6 31 | 32 | packages: 33 | 34 | /@esbuild/android-arm64@0.18.11: 35 | resolution: {integrity: sha512-snieiq75Z1z5LJX9cduSAjUr7vEI1OdlzFPMw0HH5YI7qQHDd3qs+WZoMrWYDsfRJSq36lIA6mfZBkvL46KoIw==} 36 | engines: {node: '>=12'} 37 | cpu: [arm64] 38 | os: [android] 39 | requiresBuild: true 40 | dev: true 41 | optional: true 42 | 43 | /@esbuild/android-arm@0.18.11: 44 | resolution: {integrity: sha512-q4qlUf5ucwbUJZXF5tEQ8LF7y0Nk4P58hOsGk3ucY0oCwgQqAnqXVbUuahCddVHfrxmpyewRpiTHwVHIETYu7Q==} 45 | engines: {node: '>=12'} 46 | cpu: [arm] 47 | os: [android] 48 | requiresBuild: true 49 | dev: true 50 | optional: true 51 | 52 | /@esbuild/android-x64@0.18.11: 53 | resolution: {integrity: sha512-iPuoxQEV34+hTF6FT7om+Qwziv1U519lEOvekXO9zaMMlT9+XneAhKL32DW3H7okrCOBQ44BMihE8dclbZtTuw==} 54 | engines: {node: '>=12'} 55 | cpu: [x64] 56 | os: [android] 57 | requiresBuild: true 58 | dev: true 59 | optional: true 60 | 61 | /@esbuild/darwin-arm64@0.18.11: 62 | resolution: {integrity: sha512-Gm0QkI3k402OpfMKyQEEMG0RuW2LQsSmI6OeO4El2ojJMoF5NLYb3qMIjvbG/lbMeLOGiW6ooU8xqc+S0fgz2w==} 63 | engines: {node: '>=12'} 64 | cpu: [arm64] 65 | os: [darwin] 66 | requiresBuild: true 67 | dev: true 68 | optional: true 69 | 70 | /@esbuild/darwin-x64@0.18.11: 71 | resolution: {integrity: sha512-N15Vzy0YNHu6cfyDOjiyfJlRJCB/ngKOAvoBf1qybG3eOq0SL2Lutzz9N7DYUbb7Q23XtHPn6lMDF6uWbGv9Fw==} 72 | engines: {node: '>=12'} 73 | cpu: [x64] 74 | os: [darwin] 75 | requiresBuild: true 76 | dev: true 77 | optional: true 78 | 79 | /@esbuild/freebsd-arm64@0.18.11: 80 | resolution: {integrity: sha512-atEyuq6a3omEY5qAh5jIORWk8MzFnCpSTUruBgeyN9jZq1K/QI9uke0ATi3MHu4L8c59CnIi4+1jDKMuqmR71A==} 81 | engines: {node: '>=12'} 82 | cpu: [arm64] 83 | os: [freebsd] 84 | requiresBuild: true 85 | dev: true 86 | optional: true 87 | 88 | /@esbuild/freebsd-x64@0.18.11: 89 | resolution: {integrity: sha512-XtuPrEfBj/YYYnAAB7KcorzzpGTvOr/dTtXPGesRfmflqhA4LMF0Gh/n5+a9JBzPuJ+CGk17CA++Hmr1F/gI0Q==} 90 | engines: {node: '>=12'} 91 | cpu: [x64] 92 | os: [freebsd] 93 | requiresBuild: true 94 | dev: true 95 | optional: true 96 | 97 | /@esbuild/linux-arm64@0.18.11: 98 | resolution: {integrity: sha512-c6Vh2WS9VFKxKZ2TvJdA7gdy0n6eSy+yunBvv4aqNCEhSWVor1TU43wNRp2YLO9Vng2G+W94aRz+ILDSwAiYog==} 99 | engines: {node: '>=12'} 100 | cpu: [arm64] 101 | os: [linux] 102 | requiresBuild: true 103 | dev: true 104 | optional: true 105 | 106 | /@esbuild/linux-arm@0.18.11: 107 | resolution: {integrity: sha512-Idipz+Taso/toi2ETugShXjQ3S59b6m62KmLHkJlSq/cBejixmIydqrtM2XTvNCywFl3VC7SreSf6NV0i6sRyg==} 108 | engines: {node: '>=12'} 109 | cpu: [arm] 110 | os: [linux] 111 | requiresBuild: true 112 | dev: true 113 | optional: true 114 | 115 | /@esbuild/linux-ia32@0.18.11: 116 | resolution: {integrity: sha512-S3hkIF6KUqRh9n1Q0dSyYcWmcVa9Cg+mSoZEfFuzoYXXsk6196qndrM+ZiHNwpZKi3XOXpShZZ+9dfN5ykqjjw==} 117 | engines: {node: '>=12'} 118 | cpu: [ia32] 119 | os: [linux] 120 | requiresBuild: true 121 | dev: true 122 | optional: true 123 | 124 | /@esbuild/linux-loong64@0.18.11: 125 | resolution: {integrity: sha512-MRESANOoObQINBA+RMZW+Z0TJWpibtE7cPFnahzyQHDCA9X9LOmGh68MVimZlM9J8n5Ia8lU773te6O3ILW8kw==} 126 | engines: {node: '>=12'} 127 | cpu: [loong64] 128 | os: [linux] 129 | requiresBuild: true 130 | dev: true 131 | optional: true 132 | 133 | /@esbuild/linux-mips64el@0.18.11: 134 | resolution: {integrity: sha512-qVyPIZrXNMOLYegtD1u8EBccCrBVshxMrn5MkuFc3mEVsw7CCQHaqZ4jm9hbn4gWY95XFnb7i4SsT3eflxZsUg==} 135 | engines: {node: '>=12'} 136 | cpu: [mips64el] 137 | os: [linux] 138 | requiresBuild: true 139 | dev: true 140 | optional: true 141 | 142 | /@esbuild/linux-ppc64@0.18.11: 143 | resolution: {integrity: sha512-T3yd8vJXfPirZaUOoA9D2ZjxZX4Gr3QuC3GztBJA6PklLotc/7sXTOuuRkhE9W/5JvJP/K9b99ayPNAD+R+4qQ==} 144 | engines: {node: '>=12'} 145 | cpu: [ppc64] 146 | os: [linux] 147 | requiresBuild: true 148 | dev: true 149 | optional: true 150 | 151 | /@esbuild/linux-riscv64@0.18.11: 152 | resolution: {integrity: sha512-evUoRPWiwuFk++snjH9e2cAjF5VVSTj+Dnf+rkO/Q20tRqv+644279TZlPK8nUGunjPAtQRCj1jQkDAvL6rm2w==} 153 | engines: {node: '>=12'} 154 | cpu: [riscv64] 155 | os: [linux] 156 | requiresBuild: true 157 | dev: true 158 | optional: true 159 | 160 | /@esbuild/linux-s390x@0.18.11: 161 | resolution: {integrity: sha512-/SlRJ15XR6i93gRWquRxYCfhTeC5PdqEapKoLbX63PLCmAkXZHY2uQm2l9bN0oPHBsOw2IswRZctMYS0MijFcg==} 162 | engines: {node: '>=12'} 163 | cpu: [s390x] 164 | os: [linux] 165 | requiresBuild: true 166 | dev: true 167 | optional: true 168 | 169 | /@esbuild/linux-x64@0.18.11: 170 | resolution: {integrity: sha512-xcncej+wF16WEmIwPtCHi0qmx1FweBqgsRtEL1mSHLFR6/mb3GEZfLQnx+pUDfRDEM4DQF8dpXIW7eDOZl1IbA==} 171 | engines: {node: '>=12'} 172 | cpu: [x64] 173 | os: [linux] 174 | requiresBuild: true 175 | dev: true 176 | optional: true 177 | 178 | /@esbuild/netbsd-x64@0.18.11: 179 | resolution: {integrity: sha512-aSjMHj/F7BuS1CptSXNg6S3M4F3bLp5wfFPIJM+Km2NfIVfFKhdmfHF9frhiCLIGVzDziggqWll0B+9AUbud/Q==} 180 | engines: {node: '>=12'} 181 | cpu: [x64] 182 | os: [netbsd] 183 | requiresBuild: true 184 | dev: true 185 | optional: true 186 | 187 | /@esbuild/openbsd-x64@0.18.11: 188 | resolution: {integrity: sha512-tNBq+6XIBZtht0xJGv7IBB5XaSyvYPCm1PxJ33zLQONdZoLVM0bgGqUrXnJyiEguD9LU4AHiu+GCXy/Hm9LsdQ==} 189 | engines: {node: '>=12'} 190 | cpu: [x64] 191 | os: [openbsd] 192 | requiresBuild: true 193 | dev: true 194 | optional: true 195 | 196 | /@esbuild/sunos-x64@0.18.11: 197 | resolution: {integrity: sha512-kxfbDOrH4dHuAAOhr7D7EqaYf+W45LsAOOhAet99EyuxxQmjbk8M9N4ezHcEiCYPaiW8Dj3K26Z2V17Gt6p3ng==} 198 | engines: {node: '>=12'} 199 | cpu: [x64] 200 | os: [sunos] 201 | requiresBuild: true 202 | dev: true 203 | optional: true 204 | 205 | /@esbuild/win32-arm64@0.18.11: 206 | resolution: {integrity: sha512-Sh0dDRyk1Xi348idbal7lZyfSkjhJsdFeuC13zqdipsvMetlGiFQNdO+Yfp6f6B4FbyQm7qsk16yaZk25LChzg==} 207 | engines: {node: '>=12'} 208 | cpu: [arm64] 209 | os: [win32] 210 | requiresBuild: true 211 | dev: true 212 | optional: true 213 | 214 | /@esbuild/win32-ia32@0.18.11: 215 | resolution: {integrity: sha512-o9JUIKF1j0rqJTFbIoF4bXj6rvrTZYOrfRcGyL0Vm5uJ/j5CkBD/51tpdxe9lXEDouhRgdr/BYzUrDOvrWwJpg==} 216 | engines: {node: '>=12'} 217 | cpu: [ia32] 218 | os: [win32] 219 | requiresBuild: true 220 | dev: true 221 | optional: true 222 | 223 | /@esbuild/win32-x64@0.18.11: 224 | resolution: {integrity: sha512-rQI4cjLHd2hGsM1LqgDI7oOCYbQ6IBOVsX9ejuRMSze0GqXUG2ekwiKkiBU1pRGSeCqFFHxTrcEydB2Hyoz9CA==} 225 | engines: {node: '>=12'} 226 | cpu: [x64] 227 | os: [win32] 228 | requiresBuild: true 229 | dev: true 230 | optional: true 231 | 232 | /@jridgewell/sourcemap-codec@1.4.15: 233 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 234 | dev: true 235 | 236 | /@mdn/browser-compat-data@5.3.0: 237 | resolution: {integrity: sha512-TrVSwoxpNKImgvHdAUDRleHJXWjBOgrri+C1OogMSwOPeIPZ0gEdym4cRQWv7VGHnkHCZ/PwR01XQaWlD00KFA==} 238 | dev: true 239 | 240 | /@rollup/plugin-node-resolve@15.1.0(rollup@3.26.1): 241 | resolution: {integrity: sha512-xeZHCgsiZ9pzYVgAo9580eCGqwh/XCEUM9q6iQfGNocjgkufHAqC3exA+45URvhiYV8sBF9RlBai650eNs7AsA==} 242 | engines: {node: '>=14.0.0'} 243 | peerDependencies: 244 | rollup: ^2.78.0||^3.0.0 245 | peerDependenciesMeta: 246 | rollup: 247 | optional: true 248 | dependencies: 249 | '@rollup/pluginutils': 5.0.2(rollup@3.26.1) 250 | '@types/resolve': 1.20.2 251 | deepmerge: 4.3.1 252 | is-builtin-module: 3.2.1 253 | is-module: 1.0.0 254 | resolve: 1.22.2 255 | rollup: 3.26.1 256 | dev: true 257 | 258 | /@rollup/pluginutils@5.0.2(rollup@3.26.1): 259 | resolution: {integrity: sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA==} 260 | engines: {node: '>=14.0.0'} 261 | peerDependencies: 262 | rollup: ^1.20.0||^2.0.0||^3.0.0 263 | peerDependenciesMeta: 264 | rollup: 265 | optional: true 266 | dependencies: 267 | '@types/estree': 1.0.1 268 | estree-walker: 2.0.2 269 | picomatch: 2.3.1 270 | rollup: 3.26.1 271 | dev: true 272 | 273 | /@types/estree@1.0.1: 274 | resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} 275 | dev: true 276 | 277 | /@types/node@17.0.45: 278 | resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} 279 | dev: true 280 | 281 | /@types/node@20.3.3: 282 | resolution: {integrity: sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==} 283 | dev: true 284 | 285 | /@types/object-path@0.11.1: 286 | resolution: {integrity: sha512-219LSCO9HPcoXcRTC6DbCs0FRhZgBnEMzf16RRqkT40WbkKx3mOeQuz3e2XqbfhOz/AHfbru0kzB1n1RCAsIIg==} 287 | dev: true 288 | 289 | /@types/resolve@1.20.2: 290 | resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} 291 | dev: true 292 | 293 | /@types/semver@7.5.0: 294 | resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==} 295 | dev: true 296 | 297 | /@types/ua-parser-js@0.7.36: 298 | resolution: {integrity: sha512-N1rW+njavs70y2cApeIw1vLMYXRwfBy+7trgavGuuTfOd7j1Yh7QTRc/yqsPl6ncokt72ZXuxEU0PiCp9bSwNQ==} 299 | dev: true 300 | 301 | /@wessberg/stringutil@1.0.19: 302 | resolution: {integrity: sha512-9AZHVXWlpN8Cn9k5BC/O0Dzb9E9xfEMXzYrNunwvkUTvuK7xgQPVRZpLo+jWCOZ5r8oBa8NIrHuPEu1hzbb6bg==} 303 | engines: {node: '>=8.0.0'} 304 | dev: true 305 | 306 | /ansi-colors@4.1.3: 307 | resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} 308 | engines: {node: '>=6'} 309 | dev: true 310 | 311 | /anymatch@3.1.3: 312 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 313 | engines: {node: '>= 8'} 314 | dependencies: 315 | normalize-path: 3.0.0 316 | picomatch: 2.3.1 317 | dev: true 318 | 319 | /binary-extensions@2.2.0: 320 | resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} 321 | engines: {node: '>=8'} 322 | dev: true 323 | 324 | /braces@3.0.2: 325 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 326 | engines: {node: '>=8'} 327 | dependencies: 328 | fill-range: 7.0.1 329 | dev: true 330 | 331 | /browserslist-generator@2.0.3: 332 | resolution: {integrity: sha512-3j8ogwvlBpOEDR3f5n1H2n5BWXqHPWi/Xm8EC1DPJy5BWl4WkSFisatBygH/L9AEmg0MtOfcR1QnMuM9XL28jA==} 333 | engines: {node: '>=16.15.1', npm: '>=7.0.0', pnpm: '>=3.2.0', yarn: '>=1.13'} 334 | dependencies: 335 | '@mdn/browser-compat-data': 5.3.0 336 | '@types/object-path': 0.11.1 337 | '@types/semver': 7.5.0 338 | '@types/ua-parser-js': 0.7.36 339 | browserslist: 4.21.9 340 | caniuse-lite: 1.0.30001512 341 | isbot: 3.6.12 342 | object-path: 0.11.8 343 | semver: 7.5.3 344 | ua-parser-js: 1.0.35 345 | dev: true 346 | 347 | /browserslist@4.21.9: 348 | resolution: {integrity: sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==} 349 | engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} 350 | hasBin: true 351 | dependencies: 352 | caniuse-lite: 1.0.30001512 353 | electron-to-chromium: 1.4.450 354 | node-releases: 2.0.12 355 | update-browserslist-db: 1.0.11(browserslist@4.21.9) 356 | dev: true 357 | 358 | /builtin-modules@3.3.0: 359 | resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} 360 | engines: {node: '>=6'} 361 | dev: true 362 | 363 | /caniuse-lite@1.0.30001512: 364 | resolution: {integrity: sha512-2S9nK0G/mE+jasCUsMPlARhRCts1ebcp2Ji8Y8PWi4NDE1iRdLCnEPHkEfeBrGC45L4isBx5ur3IQ6yTE2mRZw==} 365 | dev: true 366 | 367 | /chalk@5.3.0: 368 | resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} 369 | engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} 370 | dev: true 371 | 372 | /chokidar@3.5.3: 373 | resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} 374 | engines: {node: '>= 8.10.0'} 375 | dependencies: 376 | anymatch: 3.1.3 377 | braces: 3.0.2 378 | glob-parent: 5.1.2 379 | is-binary-path: 2.1.0 380 | is-glob: 4.0.3 381 | normalize-path: 3.0.0 382 | readdirp: 3.6.0 383 | optionalDependencies: 384 | fsevents: 2.3.2 385 | dev: true 386 | 387 | /compatfactory@2.0.9(typescript@5.1.6): 388 | resolution: {integrity: sha512-fvO+AWcmbO7P1S+A3mwm3IGr74eHMeq5ZLhNhyNQc9mVDNHT4oe0Gg0ksdIFFNXLK7k7Z/TYcLAUSQdRgh1bsA==} 389 | engines: {node: '>=14.9.0'} 390 | peerDependencies: 391 | typescript: '>=3.x || >= 4.x' 392 | dependencies: 393 | helpertypes: 0.0.19 394 | typescript: 5.1.6 395 | dev: true 396 | 397 | /consola@3.2.2: 398 | resolution: {integrity: sha512-r921u0vbF4lQsoIqYvSSER+yZLPQGijOHrYcWoCNVNBZmn/bRR+xT/DgerTze/nLD9TTGzdDa378TVhx7RDOYg==} 399 | engines: {node: ^14.18.0 || >=16.10.0} 400 | dev: true 401 | 402 | /crosspath@2.0.0: 403 | resolution: {integrity: sha512-ju88BYCQ2uvjO2bR+SsgLSTwTSctU+6Vp2ePbKPgSCZyy4MWZxYsT738DlKVRE5utUjobjPRm1MkTYKJxCmpTA==} 404 | engines: {node: '>=14.9.0'} 405 | dependencies: 406 | '@types/node': 17.0.45 407 | dev: true 408 | 409 | /debug@4.3.4: 410 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 411 | engines: {node: '>=6.0'} 412 | peerDependencies: 413 | supports-color: '*' 414 | peerDependenciesMeta: 415 | supports-color: 416 | optional: true 417 | dependencies: 418 | ms: 2.1.2 419 | dev: true 420 | 421 | /deepmerge@4.3.1: 422 | resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} 423 | engines: {node: '>=0.10.0'} 424 | dev: true 425 | 426 | /electron-to-chromium@1.4.450: 427 | resolution: {integrity: sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==} 428 | dev: true 429 | 430 | /es-module-lexer@1.3.0: 431 | resolution: {integrity: sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==} 432 | dev: true 433 | 434 | /esbuild@0.18.11: 435 | resolution: {integrity: sha512-i8u6mQF0JKJUlGR3OdFLKldJQMMs8OqM9Cc3UCi9XXziJ9WERM5bfkHaEAy0YAvPRMgqSW55W7xYn84XtEFTtA==} 436 | engines: {node: '>=12'} 437 | hasBin: true 438 | requiresBuild: true 439 | optionalDependencies: 440 | '@esbuild/android-arm': 0.18.11 441 | '@esbuild/android-arm64': 0.18.11 442 | '@esbuild/android-x64': 0.18.11 443 | '@esbuild/darwin-arm64': 0.18.11 444 | '@esbuild/darwin-x64': 0.18.11 445 | '@esbuild/freebsd-arm64': 0.18.11 446 | '@esbuild/freebsd-x64': 0.18.11 447 | '@esbuild/linux-arm': 0.18.11 448 | '@esbuild/linux-arm64': 0.18.11 449 | '@esbuild/linux-ia32': 0.18.11 450 | '@esbuild/linux-loong64': 0.18.11 451 | '@esbuild/linux-mips64el': 0.18.11 452 | '@esbuild/linux-ppc64': 0.18.11 453 | '@esbuild/linux-riscv64': 0.18.11 454 | '@esbuild/linux-s390x': 0.18.11 455 | '@esbuild/linux-x64': 0.18.11 456 | '@esbuild/netbsd-x64': 0.18.11 457 | '@esbuild/openbsd-x64': 0.18.11 458 | '@esbuild/sunos-x64': 0.18.11 459 | '@esbuild/win32-arm64': 0.18.11 460 | '@esbuild/win32-ia32': 0.18.11 461 | '@esbuild/win32-x64': 0.18.11 462 | dev: true 463 | 464 | /escalade@3.1.1: 465 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 466 | engines: {node: '>=6'} 467 | dev: true 468 | 469 | /estree-walker@2.0.2: 470 | resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} 471 | dev: true 472 | 473 | /fill-range@7.0.1: 474 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 475 | engines: {node: '>=8'} 476 | dependencies: 477 | to-regex-range: 5.0.1 478 | dev: true 479 | 480 | /fsevents@2.3.2: 481 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 482 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 483 | os: [darwin] 484 | requiresBuild: true 485 | dev: true 486 | optional: true 487 | 488 | /function-bind@1.1.1: 489 | resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} 490 | dev: true 491 | 492 | /glob-parent@5.1.2: 493 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 494 | engines: {node: '>= 6'} 495 | dependencies: 496 | is-glob: 4.0.3 497 | dev: true 498 | 499 | /has@1.0.3: 500 | resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} 501 | engines: {node: '>= 0.4.0'} 502 | dependencies: 503 | function-bind: 1.1.1 504 | dev: true 505 | 506 | /helpertypes@0.0.19: 507 | resolution: {integrity: sha512-J00e55zffgi3yVnUp0UdbMztNkr2PnizEkOe9URNohnrNhW5X0QpegkuLpOmFQInpi93Nb8MCjQRHAiCDF42NQ==} 508 | engines: {node: '>=10.0.0'} 509 | dev: true 510 | 511 | /immutable@4.3.0: 512 | resolution: {integrity: sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==} 513 | dev: true 514 | 515 | /is-binary-path@2.1.0: 516 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 517 | engines: {node: '>=8'} 518 | dependencies: 519 | binary-extensions: 2.2.0 520 | dev: true 521 | 522 | /is-builtin-module@3.2.1: 523 | resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} 524 | engines: {node: '>=6'} 525 | dependencies: 526 | builtin-modules: 3.3.0 527 | dev: true 528 | 529 | /is-core-module@2.12.1: 530 | resolution: {integrity: sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==} 531 | dependencies: 532 | has: 1.0.3 533 | dev: true 534 | 535 | /is-extglob@2.1.1: 536 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 537 | engines: {node: '>=0.10.0'} 538 | dev: true 539 | 540 | /is-glob@4.0.3: 541 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 542 | engines: {node: '>=0.10.0'} 543 | dependencies: 544 | is-extglob: 2.1.1 545 | dev: true 546 | 547 | /is-module@1.0.0: 548 | resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} 549 | dev: true 550 | 551 | /is-number@7.0.0: 552 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 553 | engines: {node: '>=0.12.0'} 554 | dev: true 555 | 556 | /isbot@3.6.12: 557 | resolution: {integrity: sha512-dGc3jRIORywaaqs4G5wj+58i5/l1eoI75q7XNiyW9Sgfoyr3QkyDZUXw+cuB7AOFq/0aruCQrGLrnKJlQarP/g==} 558 | engines: {node: '>=12'} 559 | dev: true 560 | 561 | /joycon@3.1.1: 562 | resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} 563 | engines: {node: '>=10'} 564 | dev: true 565 | 566 | /jsonc-parser@3.2.0: 567 | resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} 568 | dev: true 569 | 570 | /lru-cache@6.0.0: 571 | resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} 572 | engines: {node: '>=10'} 573 | dependencies: 574 | yallist: 4.0.0 575 | dev: true 576 | 577 | /magic-string@0.27.0: 578 | resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} 579 | engines: {node: '>=12'} 580 | dependencies: 581 | '@jridgewell/sourcemap-codec': 1.4.15 582 | dev: true 583 | 584 | /ms@2.1.2: 585 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 586 | dev: true 587 | 588 | /node-releases@2.0.12: 589 | resolution: {integrity: sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==} 590 | dev: true 591 | 592 | /normalize-path@3.0.0: 593 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 594 | engines: {node: '>=0.10.0'} 595 | dev: true 596 | 597 | /object-path@0.11.8: 598 | resolution: {integrity: sha512-YJjNZrlXJFM42wTBn6zgOJVar9KFJvzx6sTWDte8sWZF//cnjl0BxHNpfZx+ZffXX63A9q0b1zsFiBX4g4X5KA==} 599 | engines: {node: '>= 10.12.0'} 600 | dev: true 601 | 602 | /path-parse@1.0.7: 603 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 604 | dev: true 605 | 606 | /picocolors@1.0.0: 607 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 608 | dev: true 609 | 610 | /picomatch@2.3.1: 611 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 612 | engines: {node: '>=8.6'} 613 | dev: true 614 | 615 | /readdirp@3.6.0: 616 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 617 | engines: {node: '>=8.10.0'} 618 | dependencies: 619 | picomatch: 2.3.1 620 | dev: true 621 | 622 | /resolve@1.22.2: 623 | resolution: {integrity: sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==} 624 | hasBin: true 625 | dependencies: 626 | is-core-module: 2.12.1 627 | path-parse: 1.0.7 628 | supports-preserve-symlinks-flag: 1.0.0 629 | dev: true 630 | 631 | /rollup-plugin-esbuild@5.0.0(esbuild@0.18.11)(rollup@3.26.1): 632 | resolution: {integrity: sha512-1cRIOHAPh8WQgdQQyyvFdeOdxuiyk+zB5zJ5+YOwrZP4cJ0MT3Fs48pQxrZeyZHcn+klFherytILVfE4aYrneg==} 633 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 634 | peerDependencies: 635 | esbuild: '>=0.10.1' 636 | rollup: ^1.20.0 || ^2.0.0 || ^3.0.0 637 | dependencies: 638 | '@rollup/pluginutils': 5.0.2(rollup@3.26.1) 639 | debug: 4.3.4 640 | es-module-lexer: 1.3.0 641 | esbuild: 0.18.11 642 | joycon: 3.1.1 643 | jsonc-parser: 3.2.0 644 | rollup: 3.26.1 645 | transitivePeerDependencies: 646 | - supports-color 647 | dev: true 648 | 649 | /rollup-plugin-ts@3.2.0(rollup@3.26.1)(typescript@5.1.6): 650 | resolution: {integrity: sha512-KkTLVifkUexEiAXS9VtSjDrjKr0TyusmNJpb2ZTAzI9VuPumSu4AktIaVNnwv70iUEitHwZtET7OAM+5n1u1tg==} 651 | engines: {node: '>=14.9.0', npm: '>=7.0.0', pnpm: '>=3.2.0', yarn: '>=1.13'} 652 | peerDependencies: 653 | '@babel/core': '>=6.x || >=7.x' 654 | '@babel/plugin-transform-runtime': '>=6.x || >=7.x' 655 | '@babel/preset-env': '>=6.x || >=7.x' 656 | '@babel/preset-typescript': '>=6.x || >=7.x' 657 | '@babel/runtime': '>=6.x || >=7.x' 658 | '@swc/core': '>=1.x' 659 | '@swc/helpers': '>=0.2' 660 | rollup: '>=1.x || >=2.x' 661 | typescript: '>=3.2.x || >= 4.x' 662 | peerDependenciesMeta: 663 | '@babel/core': 664 | optional: true 665 | '@babel/plugin-transform-runtime': 666 | optional: true 667 | '@babel/preset-env': 668 | optional: true 669 | '@babel/preset-typescript': 670 | optional: true 671 | '@babel/runtime': 672 | optional: true 673 | '@swc/core': 674 | optional: true 675 | '@swc/helpers': 676 | optional: true 677 | dependencies: 678 | '@rollup/pluginutils': 5.0.2(rollup@3.26.1) 679 | '@wessberg/stringutil': 1.0.19 680 | ansi-colors: 4.1.3 681 | browserslist: 4.21.9 682 | browserslist-generator: 2.0.3 683 | compatfactory: 2.0.9(typescript@5.1.6) 684 | crosspath: 2.0.0 685 | magic-string: 0.27.0 686 | rollup: 3.26.1 687 | ts-clone-node: 2.0.4(typescript@5.1.6) 688 | tslib: 2.6.0 689 | typescript: 5.1.6 690 | dev: true 691 | 692 | /rollup@3.26.1: 693 | resolution: {integrity: sha512-I5gJCSpSMr3U9wv4D5YA8g7w7cj3eaSDeo7t+JcaFQOmoOUBgu4K9iMp8k3EZnwbJrjQxUMSKxMyB8qEQzzaSg==} 694 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 695 | hasBin: true 696 | optionalDependencies: 697 | fsevents: 2.3.2 698 | dev: true 699 | 700 | /sass@1.63.6: 701 | resolution: {integrity: sha512-MJuxGMHzaOW7ipp+1KdELtqKbfAWbH7OLIdoSMnVe3EXPMTmxTmlaZDCTsgIpPCs3w99lLo9/zDKkOrJuT5byw==} 702 | engines: {node: '>=14.0.0'} 703 | hasBin: true 704 | dependencies: 705 | chokidar: 3.5.3 706 | immutable: 4.3.0 707 | source-map-js: 1.0.2 708 | dev: true 709 | 710 | /semver@7.5.3: 711 | resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} 712 | engines: {node: '>=10'} 713 | hasBin: true 714 | dependencies: 715 | lru-cache: 6.0.0 716 | dev: true 717 | 718 | /source-map-js@1.0.2: 719 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 720 | engines: {node: '>=0.10.0'} 721 | dev: true 722 | 723 | /supports-preserve-symlinks-flag@1.0.0: 724 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 725 | engines: {node: '>= 0.4'} 726 | dev: true 727 | 728 | /to-regex-range@5.0.1: 729 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 730 | engines: {node: '>=8.0'} 731 | dependencies: 732 | is-number: 7.0.0 733 | dev: true 734 | 735 | /ts-clone-node@2.0.4(typescript@5.1.6): 736 | resolution: {integrity: sha512-eG6FAgmQsenhIJOIFhUcO6yyYejBKZIKcI3y21jiQmIOrth5pD6GElyPAyeihbPSyBs3u/9PVNXy+5I7jGy8jA==} 737 | engines: {node: '>=14.9.0'} 738 | peerDependencies: 739 | typescript: ^3.x || ^4.x 740 | dependencies: 741 | compatfactory: 2.0.9(typescript@5.1.6) 742 | typescript: 5.1.6 743 | dev: true 744 | 745 | /tslib@2.6.0: 746 | resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} 747 | dev: true 748 | 749 | /typescript@5.1.6: 750 | resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} 751 | engines: {node: '>=14.17'} 752 | hasBin: true 753 | dev: true 754 | 755 | /ua-parser-js@1.0.35: 756 | resolution: {integrity: sha512-fKnGuqmTBnIE+/KXSzCn4db8RTigUzw1AN0DmdU6hJovUTbYJKyqj+8Mt1c4VfRDnOVJnENmfYkIPZ946UrSAA==} 757 | dev: true 758 | 759 | /update-browserslist-db@1.0.11(browserslist@4.21.9): 760 | resolution: {integrity: sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==} 761 | hasBin: true 762 | peerDependencies: 763 | browserslist: '>= 4.21.0' 764 | dependencies: 765 | browserslist: 4.21.9 766 | escalade: 3.1.1 767 | picocolors: 1.0.0 768 | dev: true 769 | 770 | /yallist@4.0.0: 771 | resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} 772 | dev: true 773 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 2023 wibus-wee 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------