diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..4edd5acb1 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.bin filter=lfs diff=lfs merge=lfs -text diff --git a/Chiton/app.ts b/Chiton/app.ts index ee80b1c19..b82db8145 100644 --- a/Chiton/app.ts +++ b/Chiton/app.ts @@ -1,306 +1,193 @@ -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Imports import * as Sentry from '@sentry/electron' -//? We use Sentry for security. //! Sentry should be the first thing to load in the entire app. -// TODO: should also track environment -// TODO: bug reports, managed updates, etc. for electron Sentry.init({ dsn: "https://611b04549c774cf18a3cf72636dba7cb@o342681.ingest.sentry.io/5560104" }); -process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0' + import os from 'os' import { app, session, dialog, powerSaveBlocker } from 'electron' -import Register from '@Mouseion/managers/register' -import Forest from '@Mouseion/utils/logger' -import SecureCommunications from '@Chiton/utils/comms' -import Roots from '@Chiton/utils/roots' -import WindowManager from '@Chiton/utils/window-manager' -import DwarfStar from '@Chiton/cache/dwarf-star' -import GasGiant from '@Chiton/cache/gas-giant' -import GOAuth from '@Chiton/oauth/google' -import MSOAuth from '@Chiton/oauth/msft' -import Mailman from '@Chiton/mail/imap' -import CarrierPigeon from '@Chiton/mail/smtp' -import AppManager from '@Chiton/utils/app-manager' -import Composer from '@Chiton/components/composer' -import Calendar from '@Chiton/components/calendar' -import Settings from '@Chiton/components/settings' -import CookieCutter from '@Chiton/cache/templates' - -import { ElectronBlocker } from '@cliqz/adblocker-electron' -import fetch from 'cross-fetch' +import Forest from '@Iris/common/logger' +import SecureCommunications from '@Marionette/ipc' +import Roots from '@Chiton/services/roots' +import GOAuth from '@Chiton/services/oauth/google' +import MSOAuth from '@Chiton/services/oauth/microsoft' import * as child_process from 'child_process'; -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -app.commandLine.appendSwitch('disable-renderer-backgrounding') -app.commandLine.appendSwitch('disable-background-timer-throttling'); -app.commandLine.appendSwitch('disable-backgrounding-occluded-windows'); -;(async () => { //! Don't remove this -- async function to use await below - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Create our Registry for global state -const Registry = new Register() -Registry.register("ENABLE_AUDITING", false) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Communications -const comms = await SecureCommunications.init() -Registry.register("Communications", comms) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Roots (for logging but at the highest level) -const roots = new Roots("logs-roots", Registry) -Registry.register("Roots", roots) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Spawn a new Forest to use for the Main process's logs -const forest = new Forest("logs-main-process") -const Lumberjack = forest.Lumberjack -Registry.register("Lumberjack", Lumberjack) -const Log = Lumberjack("App") -//! kill error popups. ugh. so fucking annoying -dialog.showErrorBox = (title, content) => Log.warn(`Main process encountered an error. ${title}: ${content}`) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Various "session" variables -const dev = process.env.NODE_ENV === 'dev'; -const commit_hash: string = (() => { - if (dev) { - const commit_hash = child_process.execSync('git rev-parse HEAD').toString().trim() - app.setAppUserModelId("Aiko Mail (Dev)") - Log.warn("Developer mode ON - commit #", commit_hash) - return commit_hash - } - app.setAppUserModelId("Aiko Mail (Beta)") - Log.log("Developer mode OFF. Performance will reflect production.") - return os.platform() + '-' + app.getVersion() -})() -Registry.register("commit hash", commit_hash) -Registry.register("dev flag", dev) -Registry.register("user agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36 Edg/93.0.961.52") -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Window controls for the main window -Log.log("Initializing Window Manager.") - -const windowManager = new WindowManager(Registry, null, "INBOX", false) -Registry.register("Window Manager", windowManager) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? OAuth modules handle servicing OAuth requests -Log.log("Initializing OAuth modules.") - -const goauth = new GOAuth( - Registry, - '446179098641-5cafrt7dl4rsqtvi5tjccqrbknurtr7k.apps.googleusercontent.com', - undefined, //! no client secret: register it as an iOS app - ['https://mail.google.com'] -) -Registry.register("Google OAuth", goauth) -const msoauth = new MSOAuth( - Registry, - '65b77461-4950-4abb-b571-ad129d9923a3', - '8154fffe-1ce5-4712-aea5-077fdcd97b9c' -) -Registry.register("Microsoft OAuth", msoauth) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Email modules that enable IMAP/SMTP -Log.log("Building IMAP/SMTP modules.") - -const carrierPigeon = new CarrierPigeon(Registry) -Registry.register("Carrier Pigeon", carrierPigeon) -const mailman = new Mailman(Registry) -Registry.register("Mailman", mailman) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Caches, preferences, storage -Log.log("Building cache modules.") - -const dwarfStar = new DwarfStar(Registry, "dwarf-star.json") -Registry.register("Dwarf Star", dwarfStar) -const gasGiant = new GasGiant(Registry, "gas-giant") -Registry.register("Gas Giant", gasGiant) -const cookieCutter = new CookieCutter(Registry, "cookie-cutter") -Registry.register("Cookie Cutter", cookieCutter) -//? Load preferences -dwarfStar.reset() -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Other windows in component form -Log.log("Initializing secondary components.") - -const composer = new Composer(Registry) -Registry.register("Composer", composer) -const calendar = new Calendar(Registry) -Registry.register("Calendar", calendar) -const settings = new Settings(Registry) -Registry.register("Settings", settings) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? App Manager tool that handles updates -Log.log("Initializing App Manager.") - -const appManager = new AppManager(Registry, dev ? "Dev" : "Stable") -Registry.register("App Manager", appManager) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Entry script for the main window -const GLOBAL_DISABLE_AUTH=true //! FIXME: DISABLE THIS IN PROD!!!!!!!!!! - -const entry = (disable_auth=GLOBAL_DISABLE_AUTH) => { - const signed_in = dwarfStar.settings.auth.authenticated - - if (signed_in || disable_auth) { - Log.success("User is signed in, loading their inbox.") - //! FIXME: before deployment, remove commit_hash from url below - Log.shout("ENV:", process.env.NODE_ENV) - if (dev) { - windowManager.loadURL('http://localhost:4160/#' + commit_hash); - windowManager.window!.webContents.openDevTools(); - } else { - // mainWindow.removeMenu(); - windowManager.loadURL(`file://${__dirname}/../Veil/index.html`); +import autoBind from 'auto-bind'; +import SockPuppet from '@Marionette/ws/sockpuppet'; +import { RESERVED_PORTS } from '@Iris/common/port'; +import SettingsStore from '@Chiton/store/settings'; +import Inbox from '@Chiton/components/inbox'; +import { autoUpdater } from 'electron'; +import Guidepost from './services/guidepost'; +import { Singleton } from '@Iris/common/types'; + +//! Singleton +export class Chiton extends SockPuppet { + + checkInitialize(): boolean { return true } + async initialize(args: any[], success: (payload: object) => void) { return success({}) } + + readonly config = { + version: app.getVersion(), + platform: os.platform(), + devMode: process.env.NODE_ENV === 'dev', + channel: "Stable", + enableAuditing: false, + user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.82 Safari/537.36 Edg/93.0.961.52", + secrets: { + googleClientId: '446179098641-5cafrt7dl4rsqtvi5tjccqrbknurtr7k.apps.googleusercontent.com', + microsoftClientId: '65b77461-4950-4abb-b571-ad129d9923a3', + } + + } + + readonly version_hash: string + private readonly updateInterval: NodeJS.Timer | null + + readonly comms: SecureCommunications + readonly forest: Forest + readonly guidepost: Guidepost + readonly settingsStore: SettingsStore + inbox?: Inbox + + private constructor() { + //*** Electron + + //? Lumberjack + Roots.init() //! must proceed super + const forest = new Forest("logs-chiton") + super("Chiton", { + forest, + renderer: false, + }, RESERVED_PORTS.CHITON) + this.forest = forest + this.guidepost = new Guidepost() + const _this = this + + if (require('electron-squirrel-startup')) { + this.Log.error("App is being installed. Quitting to prevent unintended side effects.") + app.quit() + process.exit(0) } - } else { - Log.warn("User is not signed in, loading the signin flow.") - windowManager.loadURL("https://helloaiko.com/email/signin") - } -} - -SecureCommunications.registerBasic('reentry', () => entry()) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Initialization script for launching the main window -//? Adblock to block email trackers -//? Fetch tooling for requests -const init = async () => { + //? Marionette + this.comms = SecureCommunications.init() + + //? CLI switches + app.commandLine.appendSwitch('disable-renderer-backgrounding') + app.commandLine.appendSwitch('disable-background-timer-throttling'); + app.commandLine.appendSwitch('disable-backgrounding-occluded-windows'); + //? Kill error popups + dialog.showErrorBox = (title, content) => _this.Log.warn( + `Chiton error.\n${title}\n${content}\n--------------------` + ) + //? Prevent getting throttled + powerSaveBlocker.start('prevent-app-suspension') + //? Handle quitting on Mac + app.on("window-all-closed", () => { + // TODO: live on in the tray + if (process.platform !== 'darwin') app.quit() + }) + + + //? Fingerprinting + this.version_hash = (() => { + if (_this.config.devMode) { + const commit_hash = child_process.execSync('git rev-parse HEAD').toString().trim() + app.setAppUserModelId(`Aiko Mail (Dev) #${commit_hash.slice(0, 8)}`) + _this.Log.warn(`Enabled developer mode (#${commit_hash})`) + return commit_hash + } + app.setAppUserModelId("Aiko Mail (Beta)") + switch(this.config.platform) { + case 'win32': + return '(Windows) ' + _this.config.version; + case 'darwin': + return '(MacOS) ' + _this.config.version; + case 'linux': + return '(Linux) ' + _this.config.version; + default: + return '(Emulated) ' + _this.config.version; + } + })() + + //? Automatic Updates + if (this.config.devMode) { + this.Log.warn("Automatic updates are disabled in developer mode.") + this.updateInterval = null + } else { + autoUpdater.on("error", _this.Log.error) + autoUpdater.on("checking-for-update", () => _this.Log.log("Checking for updates...")) + autoUpdater.on("update-available", () => { + _this.Log.log("Update available! Downloading...") + _this.updateInterval?.unref() + }) + autoUpdater.on("update-not-available", () => _this.Log.success("App is up to date.")) + autoUpdater.on("before-quit-for-update", () => _this.Log.shout("Installing update...")) + // @ts-ignore: This may exist and just not be typed + autoUpdater.on("download-progress", (progress) => _this.Log.log(`Downloading update... ${progress.percent}%`)) + autoUpdater.on("update-downloaded", async (event, releaseNotes, releaseName) => { + _this.Log.success("Update is ready to install:", releaseName) + _this.inbox?.onUpdateAvailable(releaseName, releaseNotes) + }) + const feed = `https://knidos.helloaiko.com/update/${_this.config.channel}/${_this.config.platform}/${_this.config.version}` + autoUpdater.setFeedURL({ url: feed }) + autoUpdater.checkForUpdates() + this.updateInterval = setInterval(autoUpdater.checkForUpdates, 5 * 60 * 1000) + } + + //? Stores + this.settingsStore = new SettingsStore(this) + + app.once('ready', this.setup) + + autoBind(this) + } + + private async setup() { + this.deploy() + await this.guidepost.deploy() + + await this.settingsStore.deploy() + this.guidepost.register(Singleton.SETTINGS, this.settingsStore.port) + + //? Impersonate Chrome in regular fetch requests + session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { + details.requestHeaders["User-Agent"] = this.config.user_agent + callback({ cancel: false, requestHeaders: details.requestHeaders }) + }) + + this.inbox = new Inbox(this, { sudo: true }) + + const _this = this + app.on('activate', () => _this.inbox!.focus()) + + const goauth = new GOAuth(this, ["https://mail.google.com"]) + await goauth.deploy() + this.guidepost.register(Singleton.GOAUTH, goauth.port) + + const msoauth = new MSOAuth(this) + await msoauth.deploy() + this.guidepost.register(Singleton.MSOAUTH, msoauth.port) + } + + private static me?: Chiton + static init() { + if (Chiton.me) return Chiton.me + Chiton.me = new Chiton() + return Chiton.me + } + + private updateAndRestart() { + autoUpdater.quitAndInstall() + return true + } + + puppetry = { + app: { + updateAndRestart: this.updateAndRestart + }, + config: () => this.config, + } - session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { - details.requestHeaders["User-Agent"] = Registry.get("user agent") - callback({ cancel: false, requestHeaders: details.requestHeaders }); - }) - - const SentinelAdblock = await ElectronBlocker.fromPrebuiltAdsAndTracking(fetch) - - windowManager.window = WindowManager.newWindow({}) - windowManager.window.maximize() - windowManager.window.show() - windowManager.window.focus() - - entry() - - SentinelAdblock.enableBlockingInSession(windowManager.window.webContents.session) } -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? App Lifecycle Hooks -powerSaveBlocker.start('prevent-app-suspension') - -app.on("ready", init) - -app.on("window-all-closed", () => { - // TODO: live on in the tray - if (process.platform !== 'darwin') app.quit() -}) - -app.on("activate", () => windowManager.focus()) -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// - - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Check for updates -if (!dev) appManager.checkForUpdates() -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -})() //! Don't remove this -- closing tag for async - - - -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// -//? Finalize exports -export const platform = process.platform -/// ////////////////////////////////////////////////////// -/// ////////////////////////////////////////////////////// \ No newline at end of file +export default Chiton.init() \ No newline at end of file diff --git a/Chiton/cache/dwarf-star.ts b/Chiton/cache/dwarf-star.ts deleted file mode 100644 index dff4c3136..000000000 --- a/Chiton/cache/dwarf-star.ts +++ /dev/null @@ -1,100 +0,0 @@ -import path from 'path' -import fs2 from 'fs-extra' -import type SecureCommunications from '@Chiton/utils/comms' -import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' - -interface Settings { - - version: number - - auth: { - authenticated: boolean - token: string - credentials: { - email: string - password: string - } - } - - meta: { - firstTime: boolean - } - -} -const isSettings = (x: any):x is Settings => !!(x.version > 0) - -export default class DwarfStar { - - private readonly fp: string - settings: Settings - private comms: SecureCommunications - - constructor(Registry: Register, fp: string) { - switch (process.platform) { - case 'darwin': fp = path.join(process.env.HOME || "~", "Library", "Application Support", "Aiko Mail", fp); break - case 'win32': fp = path.join(process.env.APPDATA || "/c/", "Aiko Mail", fp); break - case 'linux': fp = path.join(process.env.HOME || "~", ".Aiko Mail", fp); break - } - this.fp = fp - this.comms = Registry.get("Communications") as SecureCommunications - - this.settings = DwarfStar.defaultSettings - - this.comms.register("save preferences", this.set.bind(this)) - this.comms.register("clear preferences", this.reset.bind(this)) - this.comms.register("get preferences", this.copy.bind(this)) - - autoBind(this) - } - - reset(_: {}={}) { - fs2.ensureFileSync(this.fp) - const s = fs2.readFileSync(this.fp, {encoding: "utf-8"}) - if (!(s?.length > 0)) { - this.settings = DwarfStar.defaultSettings - } - else { - const d = JSON.parse(s) - if (isSettings(d)) this.settings = d - } - return this.save() - } - - private set(d: Partial) { - this.settings = { - ...this.settings, - ...d - } - return this.save() - } - - private copy(_: {}={}) { - return JSON.parse(JSON.stringify(this.settings)) - } - - save() { - const s = JSON.stringify(this.settings) - fs2.writeFileSync(this.fp, s) - return this.settings - } - - static get defaultSettings(): Settings { - return { - - version: 1, - - auth: { - authenticated: false, - token: "", - credentials: { - email: "", - password: "" - } - }, - meta: { - firstTime: true - } - } - } -} \ No newline at end of file diff --git a/Chiton/cache/gas-giant.ts b/Chiton/cache/gas-giant.ts deleted file mode 100644 index 9f86be608..000000000 --- a/Chiton/cache/gas-giant.ts +++ /dev/null @@ -1,73 +0,0 @@ -import Storage from '@Mouseion/utils/storage' -import { session } from 'electron' -import type SecureCommunications from '@Chiton/utils/comms' -import path from 'path' -import fs2 from 'fs-extra' -import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' - -export default class GasGiant { - private readonly storage: Storage - private readonly dir: string - - constructor( - Registry: Register, - dir: string - ) { - switch (process.platform) { - case 'darwin': dir = path.join(process.env.HOME || "~", "Library", "Application Support", "Aiko Mail", dir); break - case 'win32': dir = path.join(process.env.APPDATA || "/c/", "Aiko Mail", dir); break - case 'linux': dir = path.join(process.env.HOME || "~", ".Aiko Mail", dir); break - } - this.dir = dir - - this.storage = new Storage(dir, {json: true}) - - const comms = Registry.get("Communications") as SecureCommunications - comms.register("save cache", this.save.bind(this)) - comms.register("get cache", this.load.bind(this)) - comms.register("pop cache", this.pop.bind(this)) - comms.register("kill cache", this.kill.bind(this)) - comms.register("clear all cache", this.clear.bind(this)) - - autoBind(this) - } - - private async save({key, data}: {key: string, data: any}) { - try { - await this.storage.store(key, data) - return { success: true } - } catch (e) { - return { error: e } - } - } - - private async load({key}: {key: string}) { - try { - const data = await this.storage.load(key) - return { success: true, data } - } catch (e) { - return { error: e } - } - } - - private async pop({key}: {key: string}) { - try { - const data = await this.storage.pop(key) - return { success: true, data } - } catch (e) { - return { error: e } - } - } - - private kill(opts: {}) { - fs2.removeSync(this.dir) - return { success: true } - } - - private async clear(opts: {}) { - await session.defaultSession.clearCache() - return { success: true } - } - -} \ No newline at end of file diff --git a/Chiton/components/calendar.ts b/Chiton/components/calendar.ts index c1de3937a..2c0fdd3e1 100644 --- a/Chiton/components/calendar.ts +++ b/Chiton/components/calendar.ts @@ -1,95 +1,55 @@ -import type SecureCommunications from '@Chiton/utils/comms' -import WindowManager from '@Chiton/utils/window-manager' -import type Register from '@Mouseion/managers/register' -import autoBind from 'auto-bind' -import type { BrowserWindow } from 'electron' - -export default class Calendar { - private readonly comms: SecureCommunications - private windowManager: WindowManager | null = null - lock: BrowserWindow | null = null - - constructor( - private readonly Registry: Register, - ) { - this.comms = Registry.get("Communications") as SecureCommunications - - this.comms.register("please open the calendar", this.open.bind(this)) - - autoBind(this) - } - - private open({bang, provider}: {bang: string, provider: string}) { - if (this.lock) { - this.lock.show() - this.lock.focus() - return - } - - const mainWindowManager = this.Registry.get("Window Manager") as WindowManager - - const win = WindowManager.newWindow({ - width: 800, - height: 600, - frame: true, - titleBarStyle: "default" - }) - if (mainWindowManager.window?.isFullScreen()) win.setFullScreen(true) - this.lock = win - - this.windowManager = new WindowManager(this.Registry, win, 'calendar-' + bang) - this.windowManager.window = win - - if (provider == 'outlook' || provider == 'exchange' || provider == 'microsoft') { - this.windowManager.loadURL("https://outlook.office.com/calendar/") - //this.windowManager.loadURL(`file://${__dirname}/../../public/calendar.html#${bang}`) - win.webContents.insertCSS(` - #app > div > div:nth-child(3) > div:nth-child(1) { - display: none; - } - #app > div > div:nth-child(2) > div:nth-child(1) { - display: none; - } - html[dir=ltr] .ms-Panel { - left: 0px !important; - } - `) - win.on("page-title-updated", () => { - win.webContents.insertCSS(` - #app > div > div:nth-child(3) > div:nth-child(1) { - display: none; - } - #app > div > div:nth-child(2) > div:nth-child(1) { - display: none; - } - html[dir=ltr] .ms-Panel { - left: 0px !important; - } - `) - }) - } - else if (provider == 'google') { - this.windowManager.loadURL("https://calendar.google.com/calendar/") - win.webContents.insertCSS(` - `) - win.on("page-title-updated", () => { - win.webContents.insertCSS(` - `) - }) - } else { - this.windowManager.loadURL(`file://${__dirname}/../../public/calendar.html#${bang}`) - } - - win.show() - win.focus() - const _this = this - - win.on("closed", () => { - this.windowManager = null - _this.lock = null - }) - - return {bang,} - } +import type { Chiton } from "@Chiton/app"; +import { Window } from "@Chiton/components/window"; +import { RESERVED_PORTS } from "@Iris/common/port"; +import { Singleton } from "@Iris/common/types"; +import autoBind from "auto-bind"; + +export default class Calendar extends Window { + + puppetry = { + window: { + ...(this.windowPuppetry), + setFullScreen: this.setFullScreen + } + } + + checkInitialize(): boolean { + return true + } + async initialize(args: any[], success: (payload: object) => void) { + success({}) + } + + //? persist fullscreen status + setFullScreen(s: boolean) { + super.setFullScreen(s) + const settings = this.chiton.settingsStore.settings + settings.calendar.appearance.fullscreen = s + this.chiton.settingsStore.settings = settings + return true + } + + constructor(chiton: Chiton) { + const FULLSCREEN = chiton.settingsStore.settings.calendar.appearance.fullscreen ? { + fullscreen: true + } : {} + super(chiton, "Calendar", { + closable: true, + winArgs: { + frame: true, + titleBarStyle: "default", + ...FULLSCREEN + } + }, () => chiton.guidepost.register(Singleton.CALENDAR, this.port)) + + this.win.on('enter-full-screen', () => this.setFullScreen(true)) + this.win.on('leave-full-screen', () => this.setFullScreen(false)) + + + this.loadURL(`http://localhost:${RESERVED_PORTS.VEIL}/calendar`) + this.focus() + + autoBind(this) + } } \ No newline at end of file diff --git a/Chiton/components/composer.ts b/Chiton/components/composer.ts index 798c53b7a..b00dee9ae 100644 --- a/Chiton/components/composer.ts +++ b/Chiton/components/composer.ts @@ -1,60 +1,52 @@ -import type SecureCommunications from '@Chiton/utils/comms' -import WindowManager from '@Chiton/utils/window-manager' -import type Register from '@Mouseion/managers/register' -import autoBind from 'auto-bind' -import { dialog } from 'electron' -import type { Logger, LumberjackEmployer } from '@Mouseion/utils/logger' -import writeGood from "write-good" -import fs from 'fs-extra' -import mime from 'mime' - -export default class Composer { - private readonly comms: SecureCommunications - private readonly Log: Logger - - constructor( - private readonly Registry: Register, - ) { - this.comms = Registry.get("Communications") as SecureCommunications - const Lumberjack = Registry.get("Lumberjack") as LumberjackEmployer - this.Log = Lumberjack("Composer") - - this.comms.register("please open the composer", this.open.bind(this)) - this.comms.register("please attach a file", this.getAttachment.bind(this)) - this.comms.register("please check my writing", this.getSuggestions.bind(this)) - - autoBind(this) - } - - private open({bang}: {bang: string}) { - const win = WindowManager.newWindow({ - height: 600, width: 800 - }, {spellcheck: true}) - - const windowManager = new WindowManager(this.Registry, win, 'composer-' + bang) - windowManager.window = win - - windowManager.loadURL(`file://${__dirname}/../../../public/compose.html#${bang}`) - - win.show() - win.focus() - - return {bang,} - } - - private async getAttachment() { - const downloadFolder = (() => { - switch(process.platform) { - case "win32": return `${process.env.USERPROFILE}\\Downloads` - case "darwin": return `${process.env.HOME}/Downloads` - default: return `${process.env.HOME}/Downloads` - } - })() - - const { canceled, filePaths } = await dialog.showOpenDialog({ - title: "Attach a file", - defaultPath: `${downloadFolder}/`, - filters: [ //? copilot wrote this so... I hope it works? lol +import type { Chiton } from "@Chiton/app"; +import { Window } from "@Chiton/components/window"; +import { RESERVED_PORTS } from "@Iris/common/port"; +import { Multiton } from "@Iris/common/types"; +import autoBind from "auto-bind"; +import crypto from "crypto"; +import { dialog } from "electron"; +import fs from 'fs-extra'; +import mime from 'mime'; +import writeGood from "write-good"; + +interface ComposerAttachment { + size: number + contentType: string + filepath: string +} + +export default class Composer extends Window { + + ID: string = crypto.randomBytes(6).toString('hex') + + checkInitialize(): boolean { + return true + } + async initialize(args: any[], success: (payload: object) => void) { + success({}) + } + + //? persist fullscreen status + setFullScreen(s: boolean): boolean { + super.setFullScreen(s) + return true + } + + //? allow attaching files + async attachFiles(): Promise { + const downloadFolder = (() => { + switch (process.platform) { + case "win32": return `${process.env.USERPROFILE}\\Downloads` + case "darwin": return `${process.env.HOME}/Downloads` + default: return `${process.env.HOME}/Downloads` + } + })(); + + const { canceled, filePaths } = await dialog.showOpenDialog({ + title: "Attach a file", + defaultPath: `${downloadFolder}/`, + //? copilot wrote this so... I hope it works? + filters: [ { name: "All Files", extensions: ["*"] }, { name: "PDF", extensions: ["pdf"] }, { name: "Word", extensions: ["doc", "docx"] }, @@ -65,34 +57,42 @@ export default class Composer { { name: "Audio", extensions: ["mp3", "wav", "aac"] }, { name: "Video", extensions: ["mp4", "avi", "mkv"] } ] - }) - - if (canceled || !filePaths) { - this.Log.warn("User cancelled attachment download/no filePath returned.") - return [] - } + }) + if (canceled || filePaths.length === 0) { + this.Log.warn("Did not select any files.") + return [] + } + this.Log.log("Selected files:", filePaths) + + //? Get the filesize and content type of each file + const files: ComposerAttachment[] = await Promise.all(filePaths.map(async filepath => { + const { size } = await fs.promises.stat(filepath) + const contentType = mime.getType(filepath) ?? "application/octet-stream" + return { size, contentType, filepath } + })) - //? Get the filesize and content type of each file - const files = await Promise.all(filePaths.map(async filePath => { - const { size, contentType } = await fs.promises.stat(filePath).then(stats => { - return { - size: stats.size, - // @ts-ignore - contentType: mime.getType(filePath) - } - }) - return { - size, contentType, filePath - } - })) - - this.Log.shout("Attaching:", filePaths) return files - } - - private async getSuggestions({text, opts}: { text: string, opts?: writeGood.Options }) { - const suggestions = writeGood(text, opts) - return suggestions - } - -} \ No newline at end of file + } + + //? get writing suggestions + async getWritingSuggestions(text: string, opts?: writeGood.Options): Promise { + const suggestions = writeGood(text, opts) + return suggestions + } + + constructor(chiton: Chiton) { + super(chiton, "Composer", { + closable: true, + spellcheck: true, + winArgs: { + fullscreen: chiton.settingsStore.settings.inbox.appearance.fullscreen + } + }, () => chiton.guidepost.add(Multiton.COMPOSER, this.ID, this.port)) + + this.focus() + this.loadURL(`http://localhost:${RESERVED_PORTS.VEIL}/composer/${this.ID}`) + + autoBind(this) + } + +} diff --git a/Chiton/components/inbox.ts b/Chiton/components/inbox.ts new file mode 100644 index 000000000..9c6183d12 --- /dev/null +++ b/Chiton/components/inbox.ts @@ -0,0 +1,87 @@ +import type { Chiton } from "@Chiton/app"; +import { Window } from "@Chiton/components/window"; +import { RESERVED_PORTS } from "@Iris/common/port"; +import { Singleton } from "@Iris/common/types"; +import { ElectronBlocker } from "@cliqz/adblocker-electron"; +import autoBind from "auto-bind"; +import fetch from "cross-fetch"; + +export enum InboxEvents { + UPDATE_AVAILABLE="update-available", +} + +export default class Inbox extends Window { + + puppetry = { + window: { + ...(this.windowPuppetry), + setFullScreen: this.setFullScreen + } + } + + ADBLOCK_ON: boolean = false + + checkInitialize(): boolean { + return this.ADBLOCK_ON + } + async initialize(args: any[], success: (payload: object) => void) { + const adblock = await ElectronBlocker.fromPrebuiltAdsAndTracking(fetch) + adblock.enableBlockingInSession(this.win.webContents.session) + this.ADBLOCK_ON = true + success({}) + } + + //? persist fullscreen status + setFullScreen(s: boolean) { + super.setFullScreen(s) + const settings = this.chiton.settingsStore.settings + settings.inbox.appearance.fullscreen = s + this.chiton.settingsStore.settings = settings + return true + } + + onUpdateAvailable(releaseName: string, releaseNotes: string) { + this.trigger(InboxEvents.UPDATE_AVAILABLE, { + releaseName, + releaseNotes, + }) + } + + constructor(chiton: Chiton, { + sudo=false + }: { + sudo?: boolean, + }={}) { + const FULLSCREEN = chiton.settingsStore.settings.inbox.appearance.fullscreen ? { + fullscreen: true + } : {} + super(chiton, "Inbox", { + closable: false, + winArgs: { + ...FULLSCREEN + } + }, () => chiton.guidepost.register(Singleton.INBOX, this.port)) + + if (sudo || chiton.settingsStore.get().auth.authenticated) { + if (sudo) this.Log.shout("Env:", process.env.NODE_ENV, "[SUDO]") + else this.Log.shout("Env:", process.env.NODE_ENV) + if (chiton.config.devMode) { + //this.loadURL(`http://localhost:${RESERVED_PORTS.VEIL}#${chiton.version_hash}`) + // TODO: bind to settings + this.loadURL(`http://localhost:${RESERVED_PORTS.VEIL}#${chiton.settingsStore.settings.appearance.accentColor}`) + this.win.webContents.openDevTools() + } else { + this.loadURL(`file://${__dirname}/../Veil/index.html`) + } + } else { + this.Log.warn("User is not signed in, initiating login flow.") + this.loadURL("https://aikomail.com/email/signin") //! FIXME: replace with Ovid + } + + this.win.on('enter-full-screen', () => this.setFullScreen(true)) + this.win.on('leave-full-screen', () => this.setFullScreen(false)) + + autoBind(this) + } + +} \ No newline at end of file diff --git a/Chiton/utils/preload.ts b/Chiton/components/preload.ts similarity index 59% rename from Chiton/utils/preload.ts rename to Chiton/components/preload.ts index 72a482009..ed22de8c0 100644 --- a/Chiton/utils/preload.ts +++ b/Chiton/components/preload.ts @@ -1,6 +1,8 @@ import { ipcRenderer, contextBridge } from 'electron' + contextBridge.exposeInMainWorld('platform', process.platform) -contextBridge.exposeInMainWorld('ipcRenderer', ipcRenderer) -contextBridge.exposeInMainWorld("api", { + +contextBridge.exposeInMainWorld('ChitonVeryInsecureIPC', ipcRenderer) +contextBridge.exposeInMainWorld("ChitonInsecureIPC", { ipcHandler: (event: string, cb: any) => ipcRenderer.on(event, cb), }) \ No newline at end of file diff --git a/Chiton/components/settings.ts b/Chiton/components/settings.ts deleted file mode 100644 index 64418d64c..000000000 --- a/Chiton/components/settings.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type SecureCommunications from '@Chiton/utils/comms' -import WindowManager from '@Chiton/utils/window-manager' -import type Register from '@Mouseion/managers/register' -import autoBind from 'auto-bind' -import type { BrowserWindow } from 'electron' - -export default class Settings { - private readonly comms: SecureCommunications - lock: BrowserWindow | null = null - private readonly windowManager: WindowManager - - constructor( - private readonly Registry: Register, - ) { - this.comms = Registry.get("Communications") as SecureCommunications - this.windowManager = Registry.get("Window Manager") as WindowManager - - this.comms.register("please open settings", this.open.bind(this)) - - autoBind(this) - } - - private open({bang}: {bang: string}) { - if (this.lock) { - this.lock.show() - this.lock.focus() - return - } - - const win = WindowManager.newWindow({ - width: this.windowManager.window?.getBounds().width || 800, - height: this.windowManager.window?.getBounds().height || 600 - }) - if (this.windowManager.window?.isFullScreen()) win.setFullScreen(true) - this.lock = win - - const windowManager = new WindowManager(this.Registry, win, 'settings-' + bang) - windowManager.window = win - - windowManager.loadURL(`file://${__dirname}/../../../public/settings.html#${bang}`) - - win.show() - win.focus() - const _this = this - win.on("closed", () => _this.lock = null) - - return {bang,} - } - -} \ No newline at end of file diff --git a/Chiton/components/window.ts b/Chiton/components/window.ts new file mode 100644 index 000000000..e05b88ea2 --- /dev/null +++ b/Chiton/components/window.ts @@ -0,0 +1,126 @@ +import autoBind from 'auto-bind' +import { shell, powerMonitor, BrowserWindow, app, nativeTheme } from 'electron' +import path from 'path' +import type { Chiton } from '@Chiton/app' +import SockPuppet from '@Marionette/ws/sockpuppet' + +export enum WindowEvents { + FULLSCREEN="fullscreen", + MAXIMIZE="maximize", + BEFORE_QUIT="before-quit", + WOKE_FROM_SLEEP="woke-from-sleep", + RESIZE="resize", + OS_THEME_CHANGED="os-theme-changed", +} + +export abstract class Window extends SockPuppet { + + windowPuppetry = { + maximize: this.maximize, + unmaximize: this.unmaximize, + minimize: this.minimize, + setFullScreen: this.setFullScreen, + getFullScreen: this.getFullScreen, + close: this.close, + hide: this.hide, + focus: this.focus, + findInWindow: this.findInWindow, + } + + puppetry = { + window: this.windowPuppetry + } + + protected readonly win: BrowserWindow + maximize() { this.win.maximize(); return true } + unmaximize() { this.win.unmaximize(); return true } + minimize() { this.win.minimize(); return true } + setFullScreen(s: boolean) { this.win.setFullScreen(s); return true } + getFullScreen(): boolean { return this.win.isFullScreen(); return true } + close() { this.win.close(); return true } + hide() { this.win.hide(); return true } + focus() { this.win.show(); this.win.focus(); return true } + findInWindow() { this.win.webContents.findInPage(""); return true } + + protected constructor( + protected readonly chiton: Chiton, + name: string, + { + closable=true, + spellcheck=false, + winArgs={}, + }: { + closable?: boolean, + spellcheck?: boolean, + winArgs?: Partial + } ={}, + onDeploy?: () => void + ) { + super(name, { + forest: chiton.forest, + renderer: false + }) + const _this = this + + this.win = new BrowserWindow({ + show: false, + frame: process.platform == 'darwin', + titleBarStyle: 'hidden', + webPreferences: { + nodeIntegration: false, + spellcheck, + backgroundThrottling: false, + preload: path.join(__dirname, 'preload.js'), //! FIXME: migrate away from preload + }, + icon: process.platform == 'darwin' ? './icon-darwin.png' : './icon-win32.png', + roundedCorners: true, + vibrancy: "under-window", + visualEffectState: "active", + ...winArgs + }) + + this.win.on('enter-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, true)) + this.win.on('leave-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, false)) + this.win.on('enter-html-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, true)) + this.win.on('leave-html-full-screen', () => _this.trigger(WindowEvents.FULLSCREEN, false)) + this.win.on('maximize', () => _this.trigger(WindowEvents.MAXIMIZE, true)) + this.win.on('unmaximize', () => _this.trigger(WindowEvents.MAXIMIZE, false)) + let quitting = false + app.on("before-quit", _ => { + quitting = true + _this.trigger(WindowEvents.BEFORE_QUIT, {}) + }) + this.win.on("close", e => { + if (!closable && !quitting && process.platform === "darwin") { + _this.Log.log("Preventing window from closing (hiding instead).") + e.preventDefault() + _this.win.hide() + return false + } + }) + powerMonitor.on('resume', () => _this.trigger(WindowEvents.WOKE_FROM_SLEEP, {})) + this.win.on('resize', () => _this.trigger(WindowEvents.RESIZE, {})) + nativeTheme.on('updated', () => _this.trigger(WindowEvents.OS_THEME_CHANGED, {})) + + //! force OS to handle links in default browser + this.win.webContents.setWindowOpenHandler(details => { + shell.openExternal(details.url) + return { action: "deny" } + }) + + //? If it's already fullscreen save that state + if (this.win.isFullScreen()) this.setFullScreen(true) + + this.deploy().then(onDeploy) + autoBind(this) + } + + loadURL(url: string, args?: Electron.LoadURLOptions) { + if (!this.win) throw "Window has not been set. Cannot load URL." + this.win.loadURL(url, { + userAgent: this.chiton.config.user_agent, + ...args + }) + } + +} \ No newline at end of file diff --git a/Chiton/mail/NEEDS_CLEANING b/Chiton/mail/NEEDS_CLEANING new file mode 100644 index 000000000..c87382434 --- /dev/null +++ b/Chiton/mail/NEEDS_CLEANING @@ -0,0 +1 @@ +FIXME: migrate to services/mail \ No newline at end of file diff --git a/Chiton/oauth/google.ts b/Chiton/oauth/google.ts deleted file mode 100644 index 26b525f63..000000000 --- a/Chiton/oauth/google.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { shell } from "electron" -import request from 'request' -import type Register from "@Mouseion/managers/register" -import type SecureCommunications from "@Chiton/utils/comms" -import autoBind from "auto-bind" -import { google } from 'googleapis' -import type { OAuth2Client } from "google-auth-library" -import type { Request } from 'express-serve-static-core' -import type WindowManager from "@Chiton/utils/window-manager" -import type { Logger, LumberjackEmployer } from "@Mouseion/utils/logger" - -export default class GOauth { - - private readonly comms: SecureCommunications - private readonly Log: Logger - private readonly client: OAuth2Client - private readonly windowManager: WindowManager - private tmpListener: ((code: string) => void | any) | null = null - - constructor( - private readonly Registry: Register, - private readonly clientId: string, - private readonly clientSecret: string | undefined, - private scopes: string[] - ) { - this.comms = Registry.get("Communications") as SecureCommunications - const Lumberjack = Registry.get("Lumberjack") as LumberjackEmployer - this.Log = Lumberjack("Mailman") - this.windowManager = Registry.get("Window Manager") as WindowManager - - if (!scopes.includes("profile")) scopes.push("profile") - if (!scopes.includes("email")) scopes.push("email") - - this.comms.register("please get google oauth token", this.newToken.bind(this)) - this.comms.register("please refresh google oauth token", this.refreshToken.bind(this)) - this.comms.registerGET("/oauth/google", this.getCode.bind(this)) - - this.client = new google.auth.OAuth2( - clientId, clientSecret, "http://127.0.0.1:41599/oauth/google" - ) - - autoBind(this) - } - - private getCode(req: Request) { - const codeParam = req.query.code - const code: string = codeParam ? (typeof codeParam == "string" ? codeParam : "") : "" - this.windowManager.focus() - if (this.tmpListener) return this.tmpListener(code) - else return this.Log.error("OAuth code was not caught by listener.") - } - - - private newToken({login_hint}: {login_hint?: string}) { - const _this = this - return new Promise(async (s, _) => { - const finish = (code: string) => _this.client.getToken(code).then(res => _this.client.setCredentials(res.tokens)) - - _this.client.on("tokens", tokens => { - s(tokens) - }) - - const url = _this.client.generateAuthUrl({ - access_type: "offline", - scope: _this.scopes, - login_hint, - }) - - - this.tmpListener = finish - - shell.openExternal(url) - }) - } - - private refreshToken({r_token}: {r_token: string}) { - const _this = this - return new Promise(async (s, _) => { - const opts = { - method: 'POST', - url: 'https://oauth2.googleapis.com/token', - headers: { - 'content-type': 'application/x-www-form-urlencoded' - }, - form: { - client_id: _this.clientId, - client_secret: _this.clientSecret, - refresh_token: r_token, - grant_type: 'refresh_token' - } - } - - request(opts, (e, _, b) => { - if (e) return s({ error: e }) - const d = JSON.parse(b) - s(d) - }) - }) - } - -} \ No newline at end of file diff --git a/Chiton/services/guidepost.ts b/Chiton/services/guidepost.ts new file mode 100644 index 000000000..5077bb56c --- /dev/null +++ b/Chiton/services/guidepost.ts @@ -0,0 +1,72 @@ +import { RESERVED_PORTS } from "@Iris/common/port"; +import { Singleton, Multiton, type Maybe } from "@Iris/common/types"; +import SockPuppet from "@Marionette/ws/sockpuppet"; +import autoBind from "auto-bind"; +//? For reserved ports use the definition from @Iris/common/ports.ts + +export default class Guidepost extends SockPuppet { + + puppetry = { + get: { + singleton: this.getSingleton, + multiton: this.getMultiton, + }, + set: { + register: this.register, + add: this.add, + remove: this.remove, + } + } + + private SINGLETONS: Record> = { + [Singleton.INBOX]: null, + [Singleton.CALENDAR]: null, + [Singleton.GOAUTH]: null, + [Singleton.MSOAUTH]: null, + [Singleton.SETTINGS]: null, + [Singleton.TEMPLATES]: null, + } + private MULTITONS: Record}> = { + [Multiton.COMPOSER]: {}, + } + + protected checkInitialize(): boolean { return true } + protected initialize = async (args: any[], success: (payload: object) => void) => success({}) + + constructor() { + super("Guidepost", { + renderer: false + }, RESERVED_PORTS.GUIDEPOST) + + autoBind(this) + } + + public register(service: Singleton, port: number): void { + if (this.SINGLETONS[service] !== null) + return this.Log.warn(`${service} is already registered and will not be overwritten.`) + this.SINGLETONS[service] = port + } + public getSingleton(service: Singleton): number { + const port = this.SINGLETONS[service] + if (!port) throw new Error(`${service} is not registered.`) + return port + } + + public add(service: Multiton, hash: string, port: number): void { + if (this.MULTITONS[service][hash] !== null) + return this.Log.warn(`${service}:${hash} is already registered and will not be overwritten.`) + this.MULTITONS[service][hash] = port + } + public remove(service: Multiton, hash: string): void { + if (this.MULTITONS[service][hash] === null) + return this.Log.warn(`${service}:${hash} is not registered and cannot be removed.`) + this.MULTITONS[service][hash] = null + } + public getMultiton(service: Multiton, hash: string): number { + const port = this.MULTITONS[service][hash] + if (!port) throw new Error(`${service}:${hash} is not registered.`) + return port + } + + +} diff --git a/Chiton/services/oauth/google.ts b/Chiton/services/oauth/google.ts new file mode 100644 index 000000000..a45298a29 --- /dev/null +++ b/Chiton/services/oauth/google.ts @@ -0,0 +1,103 @@ +import { shell } from "electron" +import request from 'request' +import autoBind from "auto-bind" +import { google } from 'googleapis' +import type { OAuth2Client } from "google-auth-library" +import type { Request } from 'express-serve-static-core' +import SockPuppet from "@Marionette/ws/sockpuppet" +import type { Chiton } from "@Chiton/app" +import { RESERVED_PORTS } from "../guidepost" + +export default class GOAuth extends SockPuppet { + puppetry = { + authorize: this.authorize, + refresh: this.refresh + } + + protected checkInitialize(): boolean { + return true + } + protected async initialize(args: any[], success: (payload: object) => void) { + return success({}) + } + + private readonly client: OAuth2Client + private callback?: (code: string) => void | any + + constructor( + private readonly chiton: Chiton, + private readonly scopes: string[] + ) { + super("Google OAuth", { + forest: chiton.forest, + renderer: false, + }) + + if (!(this.scopes.includes('profile'))) this.scopes.push('profile') + if (!(this.scopes.includes('email'))) this.scopes.push('email') + + this.client = new google.auth.OAuth2( + this.chiton.config.secrets.googleClientId, undefined, `http://127.0.0.1:${RESERVED_PORTS.COMMS.EXPRESS}/oauth/google` + ) + + this.chiton.comms.get("/oauth/google", this.loopback.bind(this), { + respondWithClose: true + }) + + autoBind(this) + } + + private loopback(req: Request) { + const code = (typeof (req.query.code) === "string") ? req.query.code : ""; + this.chiton.inbox.focus() + this.callback!(code) + } + + // TODO: stronger types for authorize and refresh + + private authorize(login_hint?: string): Promise { + const _this = this + return new Promise(async (s, _) => { + _this.client.on('tokens', s) + + const url = _this.client.generateAuthUrl({ + access_type: "offline", + scope: _this.scopes, + login_hint, + }) + + _this.callback = async (code: string) => { + const { tokens } = await _this.client.getToken(code) + _this.client.setCredentials(tokens) + } + + shell.openExternal(url) + }) + } + + private refresh(refresh_token: string): Promise { + const _this = this + return new Promise(async (s, _) => { + const opts = { + method: 'POST', + url: 'https://oauth2.googleapis.com/token', + headers: { + 'content-type': 'application/x-www-form-urlencoded' + }, + form: { + client_id: _this.chiton.config.secrets.googleClientId, + client_secret: undefined, + refresh_token: refresh_token, + grant_type: 'refresh_token' + } + } + + request(opts, (e, _, b) => { + if (e) return s({ error: e }) + const d = JSON.parse(b) + s(d) + }) + }) + } + +} diff --git a/Chiton/oauth/msft.ts b/Chiton/services/oauth/microsoft.ts similarity index 74% rename from Chiton/oauth/msft.ts rename to Chiton/services/oauth/microsoft.ts index 11ce07d4c..33c27fde9 100644 --- a/Chiton/oauth/msft.ts +++ b/Chiton/services/oauth/microsoft.ts @@ -1,34 +1,41 @@ -import { BrowserWindow } from "electron" -import URL from 'url' +import { BrowserWindow, shell } from "electron" import request from 'request' -import type Register from "@Mouseion/managers/register" -import type SecureCommunications from "@Chiton/utils/comms" import autoBind from "auto-bind" - -export default class MSOauth { - - private readonly comms: SecureCommunications - - constructor( - Registry: Register, - private readonly clientId: string, - private readonly tenant: string - ) { - this.comms = Registry.get("Communications") as SecureCommunications - - this.comms.register("please get microsoft oauth token", this.newToken.bind(this)) - this.comms.register("please refresh microsoft oauth token", this.refreshToken.bind(this)) - - autoBind(this) - } - - private newToken({login_hint}: {login_hint?: string}) { - const _this = this - return new Promise(async (s, _) => { - - - const params = [ - `client_id=${this.clientId}`, +import SockPuppet from "@Marionette/ws/sockpuppet" +import type { Chiton } from "@Chiton/app" +import URL from "url" + +export default class MSOAuth extends SockPuppet { + puppetry = { + authorize: this.authorize, + refresh: this.refresh + } + + protected checkInitialize(): boolean { + return true + } + protected async initialize(args: any[], success: (payload: object) => void) { + return success({}) + } + + constructor( + private readonly chiton: Chiton, + ) { + super("Microsoft OAuth", { + forest: chiton.forest, + renderer: false, + }) + + autoBind(this) + } + + // TODO: stronger typing for authorize + private authorize(login_hint?: string) { + const _this = this + return new Promise(async (s, _) => { + + const params = [ + `client_id=${this.chiton.config.secrets.microsoftClientId}`, "response_type=code", "redirect_uri=https%3A%2F%2Flogin.microsoftonline.com%2Fcommon%2Foauth2%2Fnativeclient", "response_mode=query", @@ -44,9 +51,7 @@ export default class MSOauth { }) win.loadURL(url) - //! you can improve this later, the weird two-code setup is because the basic user profile token is separate - //! this is intentional in MS APIs because the M in Microsoft stands for monke - //! so we just get two codes by reloading the URL + //? you can't get both the profile and outlook tokens in one call because the M in Microsoft stands for monke let emailCode: string; const finish = (code: string | string[]) => { if (!emailCode) { @@ -57,7 +62,7 @@ export default class MSOauth { 'content-type': 'application/x-www-form-urlencoded' }, form: { - client_id: _this.clientId, + client_id: _this.chiton.config.secrets.microsoftClientId, scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send', code: code, redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient', @@ -80,7 +85,7 @@ export default class MSOauth { 'content-type': 'application/x-www-form-urlencoded' }, form: { - client_id: _this.clientId, + client_id: _this.chiton.config.secrets.microsoftClientId, scope: 'user.read', code: code, redirect_uri: 'https://login.microsoftonline.com/common/oauth2/nativeclient', @@ -129,23 +134,25 @@ export default class MSOauth { if (auth_code) finish(auth_code) } }) - - }) - } - - private refreshToken({r_token_email, r_token_profile}: {r_token_email: string, r_token_profile: string}) { - const _this = this - return new Promise(async (s, _) => { - const opts = { + }) + } + + private refresh(refresh_tokens: { + email: string, + profile: string + }): Promise { + const _this = this + return new Promise(async (s, _) => { + const opts = { method: 'POST', url: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', headers: { 'content-type': 'application/x-www-form-urlencoded' }, form: { - client_id: _this.clientId, + client_id: _this.chiton.config.secrets.microsoftClientId, scope: 'offline_access https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send', - refresh_token: r_token_email, + refresh_token: refresh_tokens.email, grant_type: 'refresh_token' } } @@ -160,9 +167,9 @@ export default class MSOauth { 'content-type': 'application/x-www-form-urlencoded' }, form: { - client_id: _this.clientId, + client_id: _this.chiton.config.secrets.microsoftClientId, scope: 'user.read', - refresh_token: r_token_profile, + refresh_token: refresh_tokens.profile, grant_type: 'refresh_token' } } @@ -176,7 +183,6 @@ export default class MSOauth { }) }) }) - }) - } - + }) + } } \ No newline at end of file diff --git a/Chiton/services/roots.ts b/Chiton/services/roots.ts new file mode 100644 index 000000000..88a18bb14 --- /dev/null +++ b/Chiton/services/roots.ts @@ -0,0 +1,74 @@ +import autoBind from 'auto-bind' +import 'colors' +import crypto from 'crypto' +import { shell } from 'electron' +import path from 'path' +import WebSocket, { Server } from 'ws' +import Storage from '@Iris/common/storage' +import fs from 'fs' +import { RESERVED_PORTS } from '@Iris/common/port' +import datapath from '@Iris/common/datapath' + +//! Roots requires Electron Shell and should ONLY operate within the main process. + +export default class Roots { + private readonly storage: Storage + private readonly id: string = crypto.randomBytes(6).toString('hex') + private constructor(logdir: string="logs-roots") { + logdir = datapath('Mouseion', logdir) + this.storage = new Storage(logdir, {json: false}) + + //? Setup simple Websockets for logging + const wss_local = new Server({ port: RESERVED_PORTS.ROOTS.LOCAL }) + const wss_remote = new Server({ port: RESERVED_PORTS.ROOTS.REMOTE }) + const _this = this + wss_local.on("connection", (ws: WebSocket) => { + ws.on("message", (m: string) => { + _this.log(m) + }) + }) + wss_remote.on("connection", (ws: WebSocket) => { + ws.on("message", (m: string) => { + try { + _this.log(m, true) + } catch (e) { + console.log(e) + } + }) + setInterval(() => ws.send("ping"), 1000) + }) + + console.log(`Roots initialized in ${this.storage.dir}/${this.id}`.green) + autoBind(this) + } + + log(msg: string, from_remote: boolean=false) { + if (from_remote) console.log(msg.replace("\n", " ").red) + this.storage.append(this.id, msg) + if (msg.includes("[ ERROR ]")) { + throw new Error(msg.split("[ ERROR ]")[1]) + } + } + + async getLogs() { + const downloadFolder = (() => { + switch(process.platform) { + case "win32": return `${process.env.USERPROFILE}\\Downloads` + case "darwin": return `${process.env.HOME}/Downloads` + default: return `${process.env.HOME}/Downloads` + } + })() + const log_dest = `${downloadFolder}/aiko-mail-${this.id}.log` + await fs.promises.copyFile(`${this.storage.dir}/${this.id}.log`, log_dest) + await shell.showItemInFolder(path.normalize(log_dest)) + } + + private static me?: Roots + + /** Do NOT call this outside the Main process. */ + static init(logdir?: string) { + if (this.me) return this.me + this.me = new Roots(logdir) + return this.me + } +} \ No newline at end of file diff --git a/Chiton/store/generic/dwarf-star.ts b/Chiton/store/generic/dwarf-star.ts new file mode 100644 index 000000000..9e73dc171 --- /dev/null +++ b/Chiton/store/generic/dwarf-star.ts @@ -0,0 +1,73 @@ +import path from 'path' +import fs2 from 'fs-extra' +import autoBind from 'auto-bind' +import SockPuppet from '@Marionette/ws/sockpuppet' +import type { Chiton } from '@Chiton/app' +import datapath from '@Iris/common/datapath' + +// TODO: this should connect to Arachne to sync settings across devices + +/** Persistent data-store for small state (e.g. settings) */ +export default abstract class DwarfStar extends SockPuppet { + puppetry = { + get: this.clone, + set: this.set, + reset: this.reset, + } + + protected state: T | null = null + protected checkInitialize(): boolean { return !!(this.state) } + protected async initialize(args: any[], success: (payload: object) => void) { + if (this.state) return this.Log.error("Already initialized.") + this.state = args[0] as T + success(this.save()) + } + + protected abstract migrations: {[version: number]: (state: any) => any} + + protected save(): T { + fs2.writeFileSync(this.fp, JSON.stringify(this.state)) + this.trigger('update', this.clone()) + return JSON.parse(fs2.readFileSync(this.fp, { encoding: "utf-8" })) as T + } + private set(state: Partial): T { + this.state = { + ...(this.state!), + ...state, + } + return this.save() + } + protected reset(version: number): T { + fs2.ensureFileSync(this.fp) + const state = JSON.parse(fs2.readFileSync(this.fp, {encoding: "utf-8"})) as T + if (!state) { + this.Log.error("Reset failed: state is empty.") + throw new Error("Cannot reset to empty state.") + } + const migrations = Object.keys(this.migrations).filter(v => +v > state.version && +v <= version) + this.state = migrations.length > 0 ? + migrations.reduce((s, v) => this.migrations[+v](s), state) + : state + return this.save() + } + private clone(): T { + return JSON.parse(JSON.stringify(this.state)); + } + + /** Utilize this only for small state. */ + constructor( + chiton: Chiton, + name: string, + private readonly fp: string, + ) { + super(name + ' (DwarfStar)', { + forest: chiton.forest, + renderer: false + }) + + this.fp = datapath(fp) + autoBind(this) + } + + public get(): T { return this.state! } +} diff --git a/Chiton/store/settings.ts b/Chiton/store/settings.ts new file mode 100644 index 000000000..9a69cd8a6 --- /dev/null +++ b/Chiton/store/settings.ts @@ -0,0 +1,85 @@ +import type { Chiton } from "@Chiton/app" +import DwarfStar from "@Chiton/store/generic/dwarf-star" +import { systemPreferences } from "electron" +import type ISettingsV1 from "@Chiton/store/types/settings/v1" +import type ISettingsV2 from "@Chiton/store/types/settings/v2" +import type ISettingsV3 from "@Chiton/store/types/settings/v3" + +export type ISettings = ISettingsV3 + +export default class SettingsStore extends DwarfStar { + + readonly VERSION = 3 + + // TODO: refactor into mutation observer + get settings() { return this.state! } + set settings(s: ISettings) { + this.state = s + this.Log.log("Updated settings") + this.save() + } + + migrations = { + 2: (state: ISettingsV1): ISettingsV2 => ({ + ...state, + appearance: { + accentColor: + process.platform == "darwin" ? + systemPreferences.getAccentColor() + : "#486fff", + theme: "auto", + }, + }), + 3: (state: ISettingsV2): ISettingsV3 => ({ + ...state, + accessibility: { + language: "en", + }, + }), + } + + constructor(chiton: Chiton) { + super(chiton, 'Settings', 'settings.json') + try { + this.Log.log("Attempting to load settings...") + this.reset(this.VERSION) + } catch { + this.Log.log("Settings not found. Initializing...") + this.state = { + version: this.VERSION, + auth: { + authenticated: false, + token: "", + credentials: { + email: "", + password: "" + } + }, + meta: { + firstTime: true + }, + appearance: { + accentColor: + process.platform == "darwin" ? + systemPreferences.getAccentColor() + : "#486fff", + theme: "auto" + }, + inbox: { + appearance: { + fullscreen: false + } + }, + calendar: { + appearance: { + fullscreen: false + } + }, + accessibility: { + language: "en" + } + } + this.save() + } + } +} \ No newline at end of file diff --git a/Chiton/cache/templates.ts b/Chiton/store/templates.ts similarity index 78% rename from Chiton/cache/templates.ts rename to Chiton/store/templates.ts index 08e55639b..5c831524a 100644 --- a/Chiton/cache/templates.ts +++ b/Chiton/store/templates.ts @@ -1,10 +1,11 @@ -import Storage from '@Mouseion/utils/storage' +import Storage from '@Iris/common/storage' import { session } from 'electron' import type SecureCommunications from '@Chiton/utils/comms' import path from 'path' import fs2 from 'fs-extra' import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' +import type Register from '@Iris/common/register' +import SockPuppet from '@Marionette/ws/sockpuppet' const HTML2Text = require('html-to-text') export interface Template { @@ -19,12 +20,25 @@ export interface TemplateEntry { preview: string } -//! FIXME: this should save to drafts +//! FIXME: this should use a DB model +// TODO: this should connect to Arachne to sync templates across devices +// TODO: convert to Aiko3 -export default class CookieCutter { - private readonly storage: Storage +export default class CookieCutter extends SockPuppet { + puppetry: { [key: string]: SockPuppetry | ((...args: any[]) => any) } + protected checkInitialize(): boolean { + throw new Error('Method not implemented.') + } + protected initialize(args: any[], success: (payload: object) => void): Promise { + throw new Error('Method not implemented.') + } + + + private readonly storage: Storage private readonly dir: string + + constructor( Registry: Register, dir: string diff --git a/Chiton/store/types/settings/v1.ts b/Chiton/store/types/settings/v1.ts new file mode 100644 index 000000000..2601521da --- /dev/null +++ b/Chiton/store/types/settings/v1.ts @@ -0,0 +1,30 @@ +export default interface ISettingsV1 { + + version: number + + auth: { + authenticated: boolean + token: string + credentials: { + email: string + password: string + } + } + + meta: { + firstTime: boolean + } + + inbox: { + appearance: { + fullscreen: boolean + } + } + + calendar: { + appearance: { + fullscreen: boolean + } + } + +} \ No newline at end of file diff --git a/Chiton/store/types/settings/v2.ts b/Chiton/store/types/settings/v2.ts new file mode 100644 index 000000000..0fe3e70a0 --- /dev/null +++ b/Chiton/store/types/settings/v2.ts @@ -0,0 +1,35 @@ +export default interface ISettingsV2 { + + version: number + + auth: { + authenticated: boolean + token: string + credentials: { + email: string + password: string + } + } + + meta: { + firstTime: boolean + } + + appearance: { + accentColor: string + theme: string + } + + inbox: { + appearance: { + fullscreen: boolean + } + } + + calendar: { + appearance: { + fullscreen: boolean + } + } + +} \ No newline at end of file diff --git a/Chiton/store/types/settings/v3.ts b/Chiton/store/types/settings/v3.ts new file mode 100644 index 000000000..2cda58fdf --- /dev/null +++ b/Chiton/store/types/settings/v3.ts @@ -0,0 +1,39 @@ +export default interface ISettingsV3 { + + version: number + + auth: { + authenticated: boolean + token: string + credentials: { + email: string + password: string + } + } + + meta: { + firstTime: boolean + } + + appearance: { + accentColor: string + theme: string + } + + inbox: { + appearance: { + fullscreen: boolean + } + } + + calendar: { + appearance: { + fullscreen: boolean + } + } + + accessibility: { + language: string // ISO 639-1 + } + +} \ No newline at end of file diff --git a/Chiton/utils/app-manager.ts b/Chiton/utils/app-manager.ts deleted file mode 100644 index 1af591b59..000000000 --- a/Chiton/utils/app-manager.ts +++ /dev/null @@ -1,81 +0,0 @@ -import os from 'os' -import { app, autoUpdater, dialog } from 'electron' -import type { Logger, LumberjackEmployer } from '@Mouseion/utils/logger' -import autoBind from 'auto-bind' -import type Register from '@Mouseion/managers/register' -import type WindowManager from '@Chiton/utils/window-manager' - -export default class AppManager { - private readonly Log: Logger - private readonly platform: NodeJS.Platform - private readonly version: string - private feed: string - private interval: NodeJS.Timer | null = null - - private readonly windowManager: WindowManager - - constructor( - Registry: Register, - private channel: string, - ) { - const Lumberjack = Registry.get("Lumberjack") as LumberjackEmployer - this.Log = Lumberjack("App Manager") - - this.windowManager = Registry.get("Window Manager") as WindowManager - - if (require('electron-squirrel-startup')) { - this.Log.error("App is being installed. Quitting to prevent unintended side effects.") - app.quit() - process.exit(0) - } - - this.platform = os.platform() - this.version = app.getVersion() - - const _this = this - autoUpdater.on("error", _this.Log.error) - autoUpdater.on("checking-for-update", () => _this.Log.log("Checking for updates...")) - autoUpdater.on("update-available", () => _this.Log.log("Update available! Downloading...")) - autoUpdater.on("update-not-available", () => _this.Log.success("App is up to date.")) - autoUpdater.on("update-downloaded", () => _this.Log.success("Downloaded update. Pending installation.")) - // @ts-ignore: This exists and is just not typed properly - autoUpdater.on("download-progress", (progressObj) => _this.Log.log(`Downloading update... ${progressObj.percent}%`)) - - autoUpdater.on("update-downloaded", async (event, releaseNotes, releaseName) => { - // TODO: replace w/ modal - const ret = await dialog.showMessageBox(_this.windowManager.window!, { - type: 'question', - buttons: ['Update', 'Later'], - defaultId: 0, - message: `An update to Aiko Mail is available. Updates contain important security updates, vital bug fixes and new features.`, - title: 'Update Available' - }) - - if (ret.response === 0) { - _this.windowManager.quitting = true - autoUpdater.quitAndInstall() - } else _this.Log.error(ret.response) - }) - - this.feed = this.getFeedURL() - this.Log.shout(`Update feed: ${this.feed}`) - - autoBind(this) - } - - private getFeedURL() { - this.feed = `https://knidos.helloaiko.com/update/${this.channel}/${this.platform}/${this.version}` - autoUpdater.setFeedURL({ - url: this.feed - }) - return this.feed - } - - public checkForUpdates() { - this.feed = this.getFeedURL() - autoUpdater.checkForUpdates() - if (!(this.interval)) { - this.interval = setInterval(autoUpdater.checkForUpdates, 5 * 60 * 1000) - } - } -} \ No newline at end of file diff --git a/Chiton/utils/roots.ts b/Chiton/utils/roots.ts deleted file mode 100644 index 9719f4e5a..000000000 --- a/Chiton/utils/roots.ts +++ /dev/null @@ -1,96 +0,0 @@ -import autoBind from 'auto-bind' -import 'colors' -import crypto from 'crypto' -import { shell } from 'electron' -import path from 'path' -import WebSocket, { Server } from 'ws' -import Storage from '@Mouseion/utils/storage' -import fs from 'fs' -import type Register from '@Mouseion/managers/register' -import type SecureCommunications from '@Chiton/utils/comms' - -//! this is the only Mouseion utility that lives outside of Mouseion -//! THIS IS ON PURPOSE. -//! Roots requires Electron Shell to work and can ONLY operate within the main process - -//! NOTHING ELSE SHOULD LIVE ON PORT 4158 OR 4159 -//! THIS IS ALSO WHY ALL OTHER PORTS IN AIKO MAIL ARE 5 DIGIT NUMBERS! -const DEFAULT_PORT = 4159 -const CLIENT_PORT = 4158 - -export default class Roots { - private readonly comms: SecureCommunications - private readonly storage: Storage - private readonly dir: string - private readonly id: string - private readonly wss: Server - private readonly wss2: Server - - constructor(dir: string="logs-roots", Registry: Register) { - this.comms = Registry.get("Communications") as SecureCommunications - this.comms.register("please get the logs", this.getLogs.bind(this)) - - //? initialize dir to the correct app datapath - const platform: string = process.platform - switch (platform) { - case 'darwin': dir = path.join( - process.env.HOME as string, 'Library', 'Application Support', - 'Aiko Mail', 'Mouseion', dir - ); break; - case 'win32': dir = path.join( - process.env.APPDATA as string, - 'Aiko Mail', 'Mouseion', dir - ); break; - case 'linux': dir = path.join( - process.env.HOME as string, - '.Aiko Mail', 'Mouseion', dir - ); break; - } - this.dir = dir - this.storage = new Storage(this.dir, {json: false}) - this.id = crypto.randomBytes(6).toString('hex') - this.wss = new Server({ port: DEFAULT_PORT }) - this.wss2 = new Server({ port: CLIENT_PORT }) - const _this = this - this.wss.on("connection", (ws: WebSocket) => { - ws.on("message", (m: string) => { - _this.log(m) - }) - }) - this.wss2.on("connection", (ws: WebSocket) => { - ws.on("message", (m: string) => { - try { - this.log(m) - } catch (e) { - console.log(e) - } - }) - setInterval(() => ws.send("ping"), 1000) - }) - - console.log(`Roots initialized in ${this.storage.dir}/${this.id}`.green.bgBlack) - autoBind(this) - } - - log(msg: string) { - if (msg.includes("[C]")) console.log(msg.replace("\n", "").black.bgGreen) - this.storage.append(this.id, msg) - if (msg.includes("[ ERROR ]")) { - throw new Error(msg.split("[ ERROR ]")[1]) - } - } - - async getLogs() { - const downloadFolder = (() => { - switch(process.platform) { - case "win32": return `${process.env.USERPROFILE}\\Downloads` - case "darwin": return `${process.env.HOME}/Downloads` - default: return `${process.env.HOME}/Downloads` - } - })() - const log_dest = `${downloadFolder}/aiko-mail-${this.id}.log` - await fs.promises.copyFile(`${this.storage.dir}/${this.id}.log`, log_dest) - await shell.showItemInFolder(path.normalize(log_dest)) - } - -} \ No newline at end of file diff --git a/Chiton/utils/window-manager.ts b/Chiton/utils/window-manager.ts deleted file mode 100644 index e1d107504..000000000 --- a/Chiton/utils/window-manager.ts +++ /dev/null @@ -1,153 +0,0 @@ -import autoBind from 'auto-bind' -import { ipcMain, shell, powerMonitor, BrowserWindow, app, nativeTheme } from 'electron' -import type { Logger, LumberjackEmployer } from '@Mouseion/utils/logger' -import type Register from '@Mouseion/managers/register' -import SecureCommunications from '@Chiton/utils/comms' -import path from 'path' - -export default class WindowManager { - private fullscreened: boolean = false - private readonly Log: Logger - public quitting: boolean = false - - constructor( - private readonly Registry: Register, - private win: BrowserWindow | null, - private readonly hash='', - private closable=true, - ) { - const _this = this - const Lumberjack = Registry.get("Lumberjack") as LumberjackEmployer - this.Log = Lumberjack("Window Manager #" + hash) - this.handler("minimize window", () => _this.minimize()) - this.handler("maximize window", () => _this.maximize()) - this.handler("unmaximize window", () => _this.unmaximize()) - this.handler("fullscreen window", () => _this.setFullScreen(true)) - this.handler("close window", () => _this.close()) - this.handler("hide window", () => _this.hide()) - this.handler("find in window", () => this.findInWindow()) - this.handler("focus window", () => this.focus()) - this.handler("get the platform", () => process.platform) - - autoBind(this) - } - - private handler(action: string, cb: any) { - SecureCommunications.registerBasic(this.hash + ': please ' + action, (_: any) => { - cb() - return true - }) - } - - maximize() { if (this.win) this.win.maximize() } - unmaximize() { if (this.win) this.win.unmaximize() } - minimize() { if (this.win) this.win.minimize() } - setFullScreen(s: boolean) { if (this.win) this.win.setFullScreen(s) } - close() { if (this.win) this.win.close() } - hide() { if (this.win) this.win.hide() } - focus() { if (this.win) { this.win.show(); this.win.focus() }} - findInWindow() { if (this.win) this.win.webContents.findInPage("") } - - set window(win: BrowserWindow | null) { - this.win = win - this.addListeners() - } - - get window() { - if (!this.win) return null; - return this.win - } - - loadURL(url: string, args?: Electron.LoadURLOptions) { - if (!this.win) throw "Window has not been set. Cannot load URL." - this.win.loadURL(url, { - userAgent: this.Registry.get("user agent"), - ...args - }) - } - - private addListeners() { - if (!(this.win)) throw "No window." - const _this = this - - const updateFullscreenStatus = (status: boolean) => { - if (_this.win) { - _this.win.webContents.send(_this.hash + ': please fullscreen status changed', status) - _this.fullscreened = status - } - } - const updateMaximizedStatus = (status: boolean) => { - if (_this.win) { - _this.win.webContents.send(_this.hash + ': please maximized status changed', status) - } - } - - this.win.on("enter-full-screen", () => updateFullscreenStatus(true)) - this.win.on("enter-html-full-screen", () => updateFullscreenStatus(true)) - this.win.on("leave-full-screen", () => updateFullscreenStatus(false)) - this.win.on("leave-html-full-screen", () => updateFullscreenStatus(false)) - - this.win.on("maximize", () => updateMaximizedStatus(true)) - this.win.on("unmaximize", () => updateMaximizedStatus(false)) - - app.on("before-quit", e => _this.quitting = true) - this.win.on("close", (e) => { - _this.Log.log(e) - if (!this.closable && !this.quitting && process.platform == "darwin") { - _this.Log.log("Preventing window from closing (hiding instead).") - e.preventDefault() - _this.win?.hide() - return false - } - }) - - powerMonitor.on("resume", () => { - try { - //? I don't think the below is necessary anymore. - // if (_this.win) _this.win.reload() - } catch (e) { - _this.Log.error(e) - } - }) - - this.win.webContents.setWindowOpenHandler(details => { - shell.openExternal(details.url) - return { action: "deny" } - }) - - //? Detect when OS color scheme changes. - nativeTheme.on("updated", () => { - _this.Log.shout("OS color scheme changed.") - _this.triggerEvent(_this.hash + ': please update color scheme', {}) - }) - - ipcMain.removeHandler(this.hash + ": please get fullscreen status") - this.handler("get fullscreen status", () => updateFullscreenStatus(_this.fullscreened)) - } - - triggerEvent(channel: string, data: any) { - if (!this.win) return this.Log.error("Tried to trigger event but window has been destroyed.") - this.win.webContents.send(channel, data) - } - - static newWindow(args: Partial, { - spellcheck=false - } ={}) { - console.log(path.join(__dirname, './public/assets/js/common/preload.js')) - return new BrowserWindow({ - show: false, - frame: process.platform == 'darwin', - titleBarStyle: 'hidden', - backgroundColor: nativeTheme.shouldUseDarkColors ? '#0c0e13' : '#ffffff', - webPreferences: { - nodeIntegration: true, //! FIXME: migrate fully to websockets - spellcheck, - backgroundThrottling: false, - preload: path.join(__dirname, 'preload.js'), - }, - icon: process.platform == 'darwin' ? './public/assets/img/icon.png' : './public/assets/img/app-icon/square-icon-shadow.png', - roundedCorners: true, - ...args - }) - } -} \ No newline at end of file diff --git a/Chiton/utils/comms.ts b/Marionette/ipc.ts similarity index 76% rename from Chiton/utils/comms.ts rename to Marionette/ipc.ts index 7f29e799c..1c430d2a2 100644 --- a/Chiton/utils/comms.ts +++ b/Marionette/ipc.ts @@ -2,17 +2,11 @@ import { ipcMain } from 'electron' import WebSocket, { Server } from 'ws' import { sign, verify } from 'jsonwebtoken' import { randomBytes } from 'crypto' -import { unused_port } from '@Mouseion/utils/marionette' +import { unused_port, RESERVED_PORTS } from '@Iris/common/port' import autoBind from 'auto-bind' import express from 'express' -const DEFAULT_PORT = 41604 -//! NEVER, NEVER, NEVER, NEVER USE PORT 41599 FOR ANYTHING ELSE -//! WE NEEEEEED 41599 FOR EXPRESS -//! IT IS A HARD-CODED REDIRECT URI PORT FOR SECURITY IN GOOGLE SIGNIN - -//! Create a new comms object for each service, as it -//! will only support a singular websocket connection (the latest is used) +//! Singleton, use extremely sparingly. export default class SecureCommunications { private readonly key: string readonly port: number @@ -25,7 +19,7 @@ export default class SecureCommunications { this.port = port this.app = express() - this.app.listen(41599, () => console.log("Comms web relay is ACTIVE".green)) + this.app.listen(RESERVED_PORTS.COMMS.EXPRESS, () => console.log("Comms web relay is ACTIVE".green)) this.key = randomBytes(32).toString('hex') console.log("New Secure Communications object created.") @@ -70,9 +64,12 @@ export default class SecureCommunications { return {stream: tag} } - static async init(): Promise { - const port = await unused_port(DEFAULT_PORT) + private static me?: SecureCommunications + static init(): SecureCommunications { + if (SecureCommunications.me) return SecureCommunications.me + const port = RESERVED_PORTS.COMMS.WS const comms = new SecureCommunications(port) + SecureCommunications.me = comms return comms } @@ -87,9 +84,10 @@ export default class SecureCommunications { return sign(payload, secret, { expiresIn: 60 * 60 * 24 * 7 }) } - register(channel: string, cb: any) { + /** @deprecated Securely handles IPC events with a custom handler callback (JWT verified). */ + on(event: string, handler: any) { const _this = this - ipcMain.handle(channel, async (_, q) => { + ipcMain.handle(event, async (_, q) => { const { token } = q let client_secret: string; @@ -100,7 +98,7 @@ export default class SecureCommunications { if (!client_secret) return { error: "Couldn't decode client secret." } try { - const payload = await cb(q) + const payload = await handler(q) if (payload?.error) return payload return { s: _this.sign(client_secret, { @@ -115,10 +113,11 @@ export default class SecureCommunications { }) } - static registerBasic(channel: string, cb: any) { - ipcMain.handle(channel, async (_, q) => { + /** @deprecated Handles IPC events with a custom handler callback, no signatures or other frills. */ + static unsafeOn(event: string, handler: any) { + ipcMain.handle(event, async (_, q) => { try { - const payload = await cb(q) + const payload = await handler(q) if (payload?.error) return payload return { success: true, payload } } catch (e) { @@ -127,7 +126,8 @@ export default class SecureCommunications { }) } - registerGET(route: string, cb: any, {respondWithClose=true}={}) { + /** Handles a GET request with a custom handler callback. */ + get(route: string, cb: any, {respondWithClose=true}={}) { this.app.get(route, (q, s) => { cb(q) //? no await -- we don't care if it actually works if (respondWithClose) return s.redirect("https://helloaiko.com/redirect") @@ -135,7 +135,3 @@ export default class SecureCommunications { }) } } - -ipcMain.handle("start new websocket server", async (_, q) => { - throw "Why are you starting a new secure comms server? Are you okay? Do you need someone to talk to?" -}) \ No newline at end of file diff --git a/Marionette/process/sockpuppet.ts b/Marionette/process/sockpuppet.ts new file mode 100644 index 000000000..5b632b45a --- /dev/null +++ b/Marionette/process/sockpuppet.ts @@ -0,0 +1,131 @@ +import { Lumberjack } from '@Iris/common/logger' +import autoBind from 'auto-bind' + +interface SockPuppetProcess extends NodeJS.Process { + send: (message: any, sendHandle?: any, options?: { + swallowErrors?: boolean | undefined; + } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean +} +type SockPuppetry = {[key: string]: (...args: any[]) => Promise | any | void} + +/* + ? Usage: + * class MySockPuppet extends SockPuppet { + * puppetry = { + * foo: async (bar: string) => something, + * bar: { + * baz: async (qux: number) => something + * } + * } + * checkInitialize(): boolean { + * return true + * } + * async initialize(args: any[], success: (payload: object) => void): Promise { + * success({ foo: "bar" }) + * } + * constructor() { + * super("MySockPuppet") + * } + * } + * const puppet = new MySockPuppet() + * puppet.deploy() + */ +export default abstract class SockPuppet extends Lumberjack { + + private readonly proc: SockPuppetProcess = process;; + private deployed: boolean = false; + abstract puppetry: SockPuppetry; + + private psucc(id: string): (payload: object) => boolean { + const proc = this.proc + return (payload: object): boolean => proc.send(JSON.stringify({ + success: true, + payload, id + })) + } + + private perr(id: string): (msg: string) => boolean { + const proc = this.proc + return (msg: string): boolean => proc.send(JSON.stringify({ + error: msg + '\n' + (new Error), + payload: {}, + success: false, + id + })) + } + + protected abstract checkInitialize(): boolean; + + protected abstract initialize(args: any[], success: (payload: object) => boolean): Promise; + + protected constructor(protected name: string, logdir?: string) { + super(name, { logdir }) + if (!process.send) throw new Error("Process was spawned without IPC and is now likely in a BAD state.") + process.title = "Aiko Mail | IPC | " + this.name + autoBind(this) + } + + /** Deploys the SockPuppet; you cannot redeploy (must do a complete teardown). */ + public deploy() { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + const _this = this + + this.proc.on('message', async (m: string): Promise => { + /* + ? m should be 'please ' + JSON stringified message + * object should have the following structure: + * { + * id: String, // some random string to make ipc easier + * action: String, + * args: [...] // must ALWAYS be set. for no args just do [] + * } + */ + + try { + + const { + id, + action, + args + }: { + id: string, + action: string, + args: any[] + } = JSON.parse(m.slice('please '.length)) + + if (!id) return _this.Log.error("No ID provided to sock puppet.") + if (!action) return _this.Log.error("No action provided to sock puppet.") + if (!Array.isArray(args)) return _this.Log.error("Args not provided to sock puppet as array.") + + const success = _this.psucc(id) + const error = _this.perr(id) + + if (!(_this.checkInitialize() || action === 'init')) + return error("Puppet has not yet been initialized.") + + const attempt = async (method: (...xs: any) => Promise | any) => { + try { + const result = await method(...args) + return success(result) + } catch (e) { + _this.Log.error(e) + if (typeof e === 'string') return error(e) + else if (e instanceof Error) return error(e.message) + else return error(JSON.stringify(e)) + } + } + + if (action === 'init') return await _this.initialize(args, success) + if (action in _this.puppetry) return await attempt(_this.puppetry[action]) + else return error("No such binding: " + action) + + } catch (e) { + return _this.proc.send(JSON.stringify({ + error: e + '\n' + (new Error) + })) + } + }) + } + +} \ No newline at end of file diff --git a/Marionette/process/sockpuppeteer.ts b/Marionette/process/sockpuppeteer.ts new file mode 100644 index 000000000..aa6e95eff --- /dev/null +++ b/Marionette/process/sockpuppeteer.ts @@ -0,0 +1,110 @@ +import path from 'path' +import { fork, ChildProcess } from 'child_process' +import crypto from 'crypto' +import Forest, { Lumberjack } from '@Iris/common/logger' +import autoBind from 'auto-bind' + +type SockPuppeteerWaiterParams = { + success: boolean, + payload: any, + error?: string, + id: string +} +type SockPuppeteerWaiter = (_: SockPuppeteerWaiterParams) => void +type SockPuppeteerListener = () => void + +type ValueType = + T extends Promise + ? U + : T;; + +type ProcessMessage = { id: string, msg: string } + +export default abstract class SockPuppeteer extends Lumberjack { + private readonly API: ChildProcess + private deployed: boolean = false; + + private readonly waiters: Record = {} + private readonly listeners: Record = {} + private readonly queue: ProcessMessage[] = [] + private rotating: boolean = false + private getID(): string { + const id = crypto.randomBytes(6).toString('hex') + if (this.waiters[id]) return this.getID() + return id + } + + /** Will fork a new process every time. */ + protected constructor(protected name: string, forest: Forest) { + super(name, { forest }) + process.title = "Aiko Mail | IPC | " + this.name + + this.API = fork(path.join(__dirname, 'puppet.js'), [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }) + this.API.stdout?.pipe(process.stdout) + this.API.stderr?.pipe(process.stderr) + autoBind(this) + } + + public deploy() { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + + //? Parses incoming messages then calls the relevant callbacks and notifies listeners + this.API.on('message', (m: string) => { + const s = JSON.parse(m) as SockPuppeteerWaiterParams + if (!(s?.id)) return this.Log.error("No ID in received message") + const cb = this.waiters[s.id] + if (!cb) return this.Log.error("No waiter set.") + const listener = this.listeners[s.id] + if (listener) listener() + cb(s) + }) + } + + private async rotate() { + if (this.queue.length > 0) { + this.rotating = true + const { id, msg } = this.queue.shift() as ProcessMessage //? TS didn't connx length > 0 to shift() != undefined + this.listeners[id] = () => { + delete this.listeners[id] + this.rotate() + } + this.API.send(msg) + } else { + this.rotating = false + } + } + + protected proxy Promise>(action: string, immediate: boolean = true) { + return (...args: Parameters): Promise>> => new Promise((s, _) => { + const id = this.getID() + const instr = { id, action, args } + + const cb: SockPuppeteerWaiter = ({ success, payload, error }: { + success: boolean, + payload: ValueType>, + error?: string + }) => { + if (error || !success) { + this.Log.error(id, '|', error || 'Failed without error.') + _() + } + else s(payload) + delete this.waiters[id] + } + + this.waiters[id] = cb + + if (!immediate) { + this.queue.push({ + id, msg: 'please ' + JSON.stringify(instr) + }) + if (!this.rotating) this.rotate() + } else { + this.API.send('please ' + JSON.stringify(instr)) + } + }) + } +} diff --git a/Marionette/puppeteers/chiton.ts b/Marionette/puppeteers/chiton.ts new file mode 100644 index 000000000..34cd6bd37 --- /dev/null +++ b/Marionette/puppeteers/chiton.ts @@ -0,0 +1,29 @@ +import type { Logger, LumberjackEmployer } from "@Iris/common/types" +import SockPuppeteer from "@Marionette/ws/sockpuppeteer" +import autoBind from "auto-bind" + +//? Import Puppet type: +import type Chiton from "@Chiton/app" +import { RESERVED_PORTS } from "@Iris/common/port" + + +export default class ChitonPuppeteer extends SockPuppeteer { + + /** Must provide logger or employer */ + constructor(opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }) { + super("Chiton", opts, RESERVED_PORTS.CHITON) + + autoBind(this) + } + + app = { + updateAndRestart: this.proxy<(typeof Chiton.puppetry.app.updateAndRestart)>("app.updateAndRestart"), + } + config = this.proxy<(typeof Chiton.puppetry.config)>("config") + + //? don't need to initialize + +} diff --git a/Marionette/puppeteers/example.ts b/Marionette/puppeteers/example.ts new file mode 100644 index 000000000..6e64d1ef6 --- /dev/null +++ b/Marionette/puppeteers/example.ts @@ -0,0 +1,36 @@ +import type { Logger, LumberjackEmployer } from "@Iris/common/types" +import SockPuppeteer from "@Marionette/ws/sockpuppeteer" +import autoBind from "auto-bind" + +//? Import Puppet type: +// import type Example from "..." + + +export default class ExamplePuppeteer extends SockPuppeteer { + + /** Must provide logger or employer */ + constructor(port: number, opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }) { + super("Example", opts, port) + + //? Triggers: + // this.register('example-event', (...) => ...) + + autoBind(this) + } + + /* + foo = { + bar: this.proxy<(typeof Example.prototype.puppetry.foo.bar)>("foo.bar"), + } + baz = this.proxy<(typeof Example.prototype.puppetry.baz)>("baz") + */ + + public async init() { + await this._init() + // ... + } + +} diff --git a/Marionette/puppeteers/generic/dwarf-star.ts b/Marionette/puppeteers/generic/dwarf-star.ts new file mode 100644 index 000000000..910d0205a --- /dev/null +++ b/Marionette/puppeteers/generic/dwarf-star.ts @@ -0,0 +1,32 @@ +import type { Logger, LumberjackEmployer } from "@Iris/common/types" +import SockPuppeteer from "@Marionette/ws/sockpuppeteer" +import autoBind from "auto-bind" + +//? Import Puppet type: +import type DwarfStar from "@Chiton/store/generic/dwarf-star" + + +export default class DwarfStarPuppeteer extends SockPuppeteer { + + /** Must provide logger or employer */ + constructor(name: string, port: number, opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }) { + super(name, opts, port) + + this.register("update", (state: IStore) => this.state = state) + + autoBind(this) + } + + state: IStore | null = null + + async get() { + this.state = await this.proxy<(typeof DwarfStar.prototype.puppetry.get)>("get")() + return this.state + } + set = this.proxy<(typeof DwarfStar.prototype.puppetry.set)>("set") + reset = this.proxy<(typeof DwarfStar.prototype.puppetry.reset)>("reset") + +} diff --git a/Marionette/puppeteers/generic/window.ts b/Marionette/puppeteers/generic/window.ts new file mode 100644 index 000000000..ebc1bf9d6 --- /dev/null +++ b/Marionette/puppeteers/generic/window.ts @@ -0,0 +1,54 @@ +import type { Logger, LumberjackEmployer } from "@Iris/common/types" +import SockPuppeteer from "@Marionette/ws/sockpuppeteer" +import autoBind from "auto-bind" +import { isFullScreen } from "@Veil/state/common" +import type { Window } from "@Chiton/components/window" + + +export default class WindowPuppeteer extends SockPuppeteer { + + private __fullscreen: boolean = false + private get _fullscreen() { + return this.__fullscreen + } + private set _fullscreen(s: boolean) { + this.__fullscreen = s + isFullScreen.value = s + } + get fullscreen() { + return this._fullscreen + } + set fullscreen(s: boolean) { + this.window.setFullScreen(s) + } + + /** Must provide logger or employer */ + constructor(name: string, port: number, opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }) { + super(name, opts, port) + + this.register('fullscreen', (s: boolean) => this._fullscreen = s) + + autoBind(this) + } + + window = { + maximize: this.proxy<(typeof Window.prototype.puppetry.window.maximize)>("window.maximize"), + unmaximize: this.proxy<(typeof Window.prototype.puppetry.window.unmaximize)>("window.unmaximize"), + minimize: this.proxy<(typeof Window.prototype.puppetry.window.minimize)>("window.minimize"), + setFullScreen: this.proxy<(typeof Window.prototype.puppetry.window.setFullScreen)>("window.setFullScreen"), + getFullScreen: this.proxy<(typeof Window.prototype.puppetry.window.getFullScreen)>("window.getFullScreen"), + close: this.proxy<(typeof Window.prototype.puppetry.window.close)>("window.close"), + hide: this.proxy<(typeof Window.prototype.puppetry.window.hide)>("window.hide"), + focus: this.proxy<(typeof Window.prototype.puppetry.window.focus)>("window.focus"), + findInWindow: this.proxy<(typeof Window.prototype.puppetry.window.findInWindow)>("window.findInWindow"), + } + + public async init() { + await this._init() + this.window.getFullScreen().then(s => this._fullscreen = s) + } + +} diff --git a/Marionette/puppeteers/guidepost.ts b/Marionette/puppeteers/guidepost.ts new file mode 100644 index 000000000..6d6666d40 --- /dev/null +++ b/Marionette/puppeteers/guidepost.ts @@ -0,0 +1,29 @@ +import type { LumberjackEmployer } from "@Iris/common/types"; +import { RESERVED_PORTS } from "@Iris/common/port"; +import SockPuppeteer from "@Marionette/ws/sockpuppeteer"; +import type Guidepost from "@Chiton/services/guidepost"; +import autoBind from "auto-bind"; +import type RemoteLogger from "@Veil/services/roots"; + +export default class GuidepostPuppeteer extends SockPuppeteer { + + /** Must provide logger or employer */ + constructor(opts: { + logger?: RemoteLogger, + employer?: LumberjackEmployer, + }) { + super("Guidepost", opts, RESERVED_PORTS.GUIDEPOST) + autoBind(this) + } + + get = { + singleton: this.proxy<(typeof Guidepost.prototype.puppetry.get.singleton)>("get.singleton"), + multiton: this.proxy<(typeof Guidepost.prototype.puppetry.get.multiton)>("get.multiton"), + } + set = { + register: this.proxy<(typeof Guidepost.prototype.puppetry.set.register)>("set.register"), + add: this.proxy<(typeof Guidepost.prototype.puppetry.set.add)>("set.add"), + remove: this.proxy<(typeof Guidepost.prototype.puppetry.set.remove)>("set.remove"), + } + +} \ No newline at end of file diff --git a/Marionette/puppeteers/inbox.ts b/Marionette/puppeteers/inbox.ts new file mode 100644 index 000000000..66025d9be --- /dev/null +++ b/Marionette/puppeteers/inbox.ts @@ -0,0 +1,17 @@ +import type { Logger, LumberjackEmployer } from "@Iris/common/types" +import autoBind from "auto-bind" +import WindowPuppeteer from "@Marionette/puppeteers/generic/window" + + +export default class InboxPuppeteer extends WindowPuppeteer { + + /** Must provide logger or employer */ + constructor(port: number, opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }) { + super("Inbox", port, opts) + autoBind(this) + } + +} diff --git a/Marionette/puppeteers/settings.ts b/Marionette/puppeteers/settings.ts new file mode 100644 index 000000000..9cb50d46e --- /dev/null +++ b/Marionette/puppeteers/settings.ts @@ -0,0 +1,12 @@ +import type { ISettings } from "@Chiton/store/settings"; +import DwarfStarPuppeteer from "./generic/dwarf-star"; +import type { Logger, LumberjackEmployer } from "@Iris/common/types"; + +export default class SettingsPuppeteer extends DwarfStarPuppeteer { + constructor(port: number, opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }) { + super("Settings", port, opts) + } +} \ No newline at end of file diff --git a/Marionette/ws/sockpuppet.ts b/Marionette/ws/sockpuppet.ts new file mode 100644 index 000000000..61ff83857 --- /dev/null +++ b/Marionette/ws/sockpuppet.ts @@ -0,0 +1,181 @@ +import WebSocket, { Server } from 'ws' +import { unused_port, RESERVED_PORTS } from '@Iris/common/port' +import Forest, { Lumberjack } from '@Iris/common/logger' +import autoBind from 'auto-bind' +import stratify from '@Iris/common/stratify' + +interface SockPuppetProcess extends NodeJS.Process { + send: (message: any, sendHandle?: any, options?: { + swallowErrors?: boolean | undefined; + } | undefined, callback?: ((error: Error | null) => void) | undefined) => boolean +} +type SockPuppetryMethod = ((...args: any[]) => Promise | any | void) +type SockPuppetry = { + [key: string]: SockPuppetryMethod | SockPuppetry +} + +/* + ! Warning: Until this is deployed, the socket doesn't exist. + ! Also, you can't expect port to be defined prior to deployment unless you pass it in. + ! It's left unprotected to allow for more complex use cases. + ! e.g. for Window management, the Window launch & load will take longer than the SockPuppet deployment. + ! So for Window management, you could expect port/socket to be reliably defined. + ? Usage: + * class MySockPuppet extends SockPuppet { + * puppetry = { + * foo: async (bar: string) => something, + * bar: { + * baz: async (qux: number) => something + * } + * } + * checkInitialize(): boolean { + * return true + * } + * async initialize(args: any[], success: (payload: object) => void): Promise { + * success({ foo: "bar" }) + * } + * constructor() { + * super("MySockPuppet") + * } + * } + * const puppet = new MySockPuppet() + * puppet.deploy() + */ +export default abstract class SockPuppet extends Lumberjack { + + private readonly proc: SockPuppetProcess | null + private deployed: boolean = false; + private readonly websockets: WebSocket[] = [] + abstract puppetry: SockPuppetry; + private API: {[key: string]: SockPuppetryMethod} = {} + + protected abstract checkInitialize(): boolean; + + protected abstract initialize(args: any[], success: (payload: object) => void): Promise; + + /** should do renderer=true if you want it to run forked */ + protected constructor( + protected name: string, + opts: { + forest?: Forest | undefined, + logdir?: string | undefined, + renderer?: boolean + }, + private _port?: number, + ) { + super(name, opts) + + if (opts.renderer) { + process.title = "Aiko Mail | WS | " + this.name + this.proc = process;; + } else this.proc = null + + autoBind(this) + } + + /** Slight safety mechanism to prevent bad accesses */ + public get port(): number { + if (!(this.deployed)) this.Log.error("Cannot get port before deployment.") + if (!(this._port)) this.Log.error("Port not defined.") + return this._port! + } + + /** Deploys the SockPuppet; you cannot redeploy (must do a complete teardown). */ + public async deploy() { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + const _this = this + + //? compile puppetry + this.API = stratify(this.puppetry) + + //? spawn websocket server + this._port = await unused_port(this._port) + const wss = new Server({ port: this._port }) + if (this.proc) this.proc.send({ port: this._port, }) + wss.on("connection", (ws: WebSocket) => { + + const succ = (id: string): ((payload?: object) => void) => { + return (payload?: object): void => ws.send(JSON.stringify({ + success: true, + payload: payload ?? {}, + id + })) + } + const err = (id: string): ((msg: string) => void) => { + return (msg: string): void => ws.send(JSON.stringify({ + error: msg + '\n' + (new Error), + payload: {}, + success: false, + id + })) + } + + _this.websockets.push(ws) + + ws.on('message', async (m: string): Promise => { + /* + ? m should be 'please ' + JSON stringified message + * object should have the following structure: + * { + * id: String, // some random string to make ipc easier + * action: String, + * args: [...] // must ALWAYS be set. for no args just do [] + * } + */ + + try { + + const { + id, + action, + args + }: { + id: string, + action: string, + args: any[] + } = JSON.parse(m.slice('please '.length)) + + if (!id) return _this.Log.error("No ID provided to sock puppet.") + if (!action) return _this.Log.error("No action provided to sock puppet.") + if (!Array.isArray(args)) return _this.Log.error("Args not provided to sock puppet as array.") + + const success = succ(id) + const error = err(id) + + if (!(_this.checkInitialize() || action === 'init')) + return error("Puppet has not yet been initialized.") + + const attempt = async (method: (...xs: any) => Promise | any) => { + try { + const result = await method(...args) + return success(result) + } catch (e) { + _this.Log.error(e) + if (typeof e === 'string') return error(e) + else if (e instanceof Error) return error(e.message) + else return error(JSON.stringify(e)) + } + } + + if (action === 'init') return await _this.initialize(args, success) + if (action in _this.API) return await attempt(_this.API[action] as SockPuppetryMethod) + else return error("No such binding: " + action + " in API:\n" + JSON.stringify(_this.API, null, 2)) + } catch (e) { + return ws.send(JSON.stringify({ + error: e + '\n' + (new Error) + })) + } + }) + }) + } + + /** Trigger an event on all puppeteers */ + protected trigger(event: string, payload: any) { + if (!this.deployed) return this.Log.error("Cannot trigger event before deployment.") + this.websockets.map(ws => ws.send(JSON.stringify({ + event, payload + }))) + } + +} \ No newline at end of file diff --git a/Marionette/ws/sockpuppeteer.ts b/Marionette/ws/sockpuppeteer.ts new file mode 100644 index 000000000..12305b909 --- /dev/null +++ b/Marionette/ws/sockpuppeteer.ts @@ -0,0 +1,181 @@ +import type { Logger, LumberjackEmployer } from '@Iris/common/types' +import autoBind from 'auto-bind' + +interface SockPuppeteerWaiterParams { + success: boolean, + payload: any, + error?: string, + id: string +} +interface SockPuppeteerTriggerParams { + event: string + payload?: any +} +const isWaiter = (t: SockPuppeteerWaiterParams | SockPuppeteerTriggerParams): t is SockPuppeteerWaiterParams => + !!((t as SockPuppeteerWaiterParams).id); +const isTrigger = (t: SockPuppeteerWaiterParams | SockPuppeteerTriggerParams): t is SockPuppeteerTriggerParams => + !!((t as SockPuppeteerTriggerParams).event); + +type SockPuppeteerWaiter = (_: SockPuppeteerWaiterParams) => void +type SockPuppeteerListener = () => void +type SockPuppeteerTrigger = (() => void) | ((_: any) => void) | (() => Promise) | ((_: any) => Promise) + +type ValueType = + T extends Promise + ? U + : T;; + +type ProcessMessage = { id: string, msg: string } + +export default abstract class SockPuppeteer { + private API?: WebSocket + protected Log: Logger + private deployed: boolean = false; + + private readonly waiters: Record = {} + private readonly listeners: Record = {} + private readonly triggers: Record = {} + + private readonly queue: ProcessMessage[] = [] + private rotating: boolean = false + + private randHex: () => string = + () => { throw new Error("Sockpuppeteer initialized without access to random hexes.") } + private getID(): string { + const id = this.randHex() + if (this.waiters[id]) return this.getID() + return id + } + + /** Leaving port empty will create a child process. */ + protected constructor(protected name: string, opts: { + logger?: Logger, + employer?: LumberjackEmployer, + }, port?: number) { + autoBind(this) + + this.Log = (() => { + if (!opts.logger) { + if (!opts.employer) throw new Error("Must provide either logger or employer") + return opts.employer(this.name) + } + return opts.logger + })();; + + if (port) { + this.randHex = () => String.random(6) + this.deploy(port) + } else { + const _this = this + ;(async () => { + process.title = "Aiko Mail | WS | " + this.name + const crypto = await import('crypto') + this.randHex = () => crypto.randomBytes(6).toString('hex') + const path = await import('path') + const { fork } = await import('child_process') + const Puppet = fork(path.join(__dirname, 'puppet.js'), [], { + stdio: ['pipe', 'pipe', 'pipe', 'ipc'] + }) + Puppet.stdout?.pipe(process.stdout) + Puppet.stderr?.pipe(process.stderr) + + //? Parses incoming messages then calls the relevant callbacks and notifies listeners + Puppet.on('message', (m: string) => { + const s = JSON.parse(m) as { port: number } + if (!(s?.port)) return _this.Log.error("No PORT specified in message") + _this.deploy(s.port) + }) + })(); + } + + autoBind(this) + } + + private async deploy(port: number) { + if (this.deployed) return this.Log.error("Already deployed.") + this.deployed = true + // connect to websocket on port s.port + const ws = new WebSocket(`ws://localhost:${port}`) + this.API = ws + ws.binaryType = 'arraybuffer' + ws.onmessage = (m: MessageEvent): any => { + const s = JSON.parse(m.data) as (SockPuppeteerWaiterParams | SockPuppeteerTriggerParams) + if (isTrigger(s)) { + const cb = this.triggers[s.event] + if (!cb) return this.Log.warn("No trigger set for", s.event) + cb(s.payload) + } else if (isWaiter(s)) { + const cb = this.waiters[s.id] + if (!cb) return this.Log.error("No waiter set.") + const listener = this.listeners[s.id] + if (listener) listener() + cb(s) + } else { + this.Log.error("Unknown message type (no id or event)", s) + } + } + } + + private async send(msg: string) { + const _this = this + return await new Promise((s, _) => { + // wait for readyState = 1 + if (this.API!.readyState === 1) s(_this.API!.send(msg)) + else setTimeout(() => _this.send(msg).then(s), 100) + }) + } + + private async rotate() { + if (this.queue.length > 0) { + this.rotating = true + const { id, msg } = this.queue.shift() as ProcessMessage //? TS didn't connx length > 0 to shift() != undefined + this.listeners[id] = () => { + delete this.listeners[id] + this.rotate() + } + this.send(msg) + } else { + this.rotating = false + } + } + + protected proxy void | any | Promise>(action: string, immediate: boolean = true) { + return (...args: Parameters): Promise>> => new Promise((s, _) => { + const id = this.getID() + const instr = { id, action, args } + + const cb: SockPuppeteerWaiter = ({ success, payload, error }: { + success: boolean, + payload: ValueType>, + error?: string + }) => { + if (error || !success) { + this.Log.error(id, '|', error || 'Failed without error.') + _() + } + else s(payload) + delete this.waiters[id] + } + + this.waiters[id] = cb + + if (!immediate) { + this.queue.push({ + id, msg: 'please ' + JSON.stringify(instr) + }) + if (!this.rotating) this.rotate() + } else { + this.send('please ' + JSON.stringify(instr)) + } + }) + } + + protected async _init(...args: any[]) { + await this.proxy("init")(...args) + } + + protected register(event: string, trigger: SockPuppeteerTrigger) { + this.triggers[event] = trigger + } + +} diff --git a/Mouseion/LIFECYCLE.md b/Mouseion/LIFECYCLE.md new file mode 100644 index 000000000..dea737b8f --- /dev/null +++ b/Mouseion/LIFECYCLE.md @@ -0,0 +1,14 @@ +# Lifecycle + +Mouseion is a singleton. + +- Can add mailboxes +- Mouseion in its lifecycle asks all mailboxes to sync +- Sync Folders -> Identify Folders & upsert +- Sync Messages, clean in-memory +- Identify Contacts & upsert +- Identify Threads & upsert +- Identify Attachments & upsert +- Identify Messages & upsert +- Run Board Rules +- diff --git a/Mouseion2/engine.ts b/Mouseion/engine.ts similarity index 100% rename from Mouseion2/engine.ts rename to Mouseion/engine.ts diff --git a/Mouseion2/server.ts b/Mouseion/server.ts similarity index 100% rename from Mouseion2/server.ts rename to Mouseion/server.ts diff --git a/Mouseion2/client.ts b/Mouseion2/client.ts deleted file mode 100644 index 43d38cbcf..000000000 --- a/Mouseion2/client.ts +++ /dev/null @@ -1,136 +0,0 @@ -import path from 'path' -import { fork, ChildProcess } from 'child_process' -import crypto from 'crypto' -import autoBind from 'auto-bind' -import type { IMAPConfig } from '@Mouseion/post-office/types' - -type SockPuppeteerWaiterParams = { - success: boolean, - payload: any, - error?: string, - id: string -} -type SockPuppeteerWaiter = (_: SockPuppeteerWaiterParams) => void -type SockPuppeteerListener = () => void - -type ValueType = - T extends Promise - ? U - : T;; - -type ProcessMessage = {id: string, msg: string} - -type PortInfo = { wsport: number } -const isPortInfo = (x: any):x is PortInfo => !!(x.wsport) - -export class EightySix { - - private readonly waiters: Record = {} - private readonly listeners: Record = {} - private readonly queue: ProcessMessage[] = [] - - private rotating: boolean = false - get isRotating() { return this.rotating } - - public port: number | null = null - - //? in theory may never terminate - private getID(): string { - const id = crypto.randomBytes(6).toString('hex') - if (this.waiters[id]) return this.getID() - return id - } - - private constructor( - public readonly API: ChildProcess, - portWaiter: (p: number) => any - ) { - //? Parses incoming messages then calls the relevant callbacks and notifies listeners - this.API.on('message', (m: string) => { - const s = JSON.parse(m) as (SockPuppeteerWaiterParams | PortInfo) - if (isPortInfo(s)) { - if (!(this.port)) { - this.port = s.wsport - console.log("EightySix connected on".magenta, this.port) - portWaiter(s.wsport) - } - return - } - if (!(s?.id)) return console.error("No ID in received message") - const cb = this.waiters[s.id] - if (!cb) return console.error("No waiter set.") - const listener = this.listeners[s.id] - if (listener) listener() - cb(s) - }) - autoBind(this) - } - - static async init(config: IMAPConfig): Promise { - let agent: EightySix; - return new Promise(async (scb, _) => { - const API = fork(path.join(__dirname, 'server.js'), [], { - stdio: ['pipe', 'pipe', 'pipe', 'ipc'] - }) - API.stdout?.pipe(process.stdout) - API.stderr?.pipe(process.stdout) - - const cb = (p: number) => { - scb(agent) - } - - agent = new this(API, cb) - await agent.proxy('init')(config) - return agent - }) - } - - - public proxy>(action: string, immediate: boolean=true) { - type Return = ValueType - return (...args: ParamType): Promise => new Promise((s, _) => { - const id = this.getID() - const instr = { id, action, args } - - const cb: SockPuppeteerWaiter = ({ success, payload, error }: { - success: boolean, - payload: Return, - error?: string - }) => { - if (error || !success) { - console.error(id, '|', error || 'Failed without error.') - _() - } - else s(payload) - delete this.waiters[id] - } - - this.waiters[id] = cb - - if (!immediate) { - this.queue.push({ - id, msg: 'please ' + JSON.stringify(instr) - }) - if (!this.rotating) this.rotate() - } else { - this.API.send('please ' + JSON.stringify(instr)) - } - - }) - } - - private async rotate() { - if (this.queue.length > 0) { - this.rotating = true - const { id, msg } = this.queue.shift()! //? TS didn't connx length > 0 to shift() defined - this.listeners[id] = () => { - delete this.listeners[id] - this.rotate() - } - this.API.send(msg) - } else { - this.rotating = false - } - } - -} \ No newline at end of file diff --git a/Mouseion/README.md b/MouseionArtifacts/README.md similarity index 100% rename from Mouseion/README.md rename to MouseionArtifacts/README.md diff --git a/Mouseion/actors/operator.ts b/MouseionArtifacts/actors/operator.ts similarity index 100% rename from Mouseion/actors/operator.ts rename to MouseionArtifacts/actors/operator.ts diff --git a/Mouseion/actors/resolver.ts b/MouseionArtifacts/actors/resolver.ts similarity index 100% rename from Mouseion/actors/resolver.ts rename to MouseionArtifacts/actors/resolver.ts diff --git a/Mouseion/actors/sync.ts b/MouseionArtifacts/actors/sync.ts similarity index 100% rename from Mouseion/actors/sync.ts rename to MouseionArtifacts/actors/sync.ts diff --git a/Mouseion/actors/tailor.ts b/MouseionArtifacts/actors/tailor.ts similarity index 100% rename from Mouseion/actors/tailor.ts rename to MouseionArtifacts/actors/tailor.ts diff --git a/Mouseion/managers/cleaners.ts b/MouseionArtifacts/managers/cleaners.ts similarity index 100% rename from Mouseion/managers/cleaners.ts rename to MouseionArtifacts/managers/cleaners.ts diff --git a/Mouseion/managers/folders.ts b/MouseionArtifacts/managers/folders.ts similarity index 100% rename from Mouseion/managers/folders.ts rename to MouseionArtifacts/managers/folders.ts diff --git a/Mouseion/managers/mailbox.ts b/MouseionArtifacts/managers/mailbox.ts similarity index 100% rename from Mouseion/managers/mailbox.ts rename to MouseionArtifacts/managers/mailbox.ts diff --git a/Mouseion/managers/register.ts b/MouseionArtifacts/managers/register.ts similarity index 100% rename from Mouseion/managers/register.ts rename to MouseionArtifacts/managers/register.ts diff --git a/Mouseion/pantheon/pantheon.ts b/MouseionArtifacts/pantheon/pantheon.ts similarity index 100% rename from Mouseion/pantheon/pantheon.ts rename to MouseionArtifacts/pantheon/pantheon.ts diff --git a/Mouseion/pantheon/puppet.ts b/MouseionArtifacts/pantheon/puppet.ts similarity index 100% rename from Mouseion/pantheon/puppet.ts rename to MouseionArtifacts/pantheon/puppet.ts diff --git a/Mouseion/pantheon/puppeteer.ts b/MouseionArtifacts/pantheon/puppeteer.ts similarity index 100% rename from Mouseion/pantheon/puppeteer.ts rename to MouseionArtifacts/pantheon/puppeteer.ts diff --git a/Mouseion/post-office/post-office.ts b/MouseionArtifacts/post-office/post-office.ts similarity index 100% rename from Mouseion/post-office/post-office.ts rename to MouseionArtifacts/post-office/post-office.ts diff --git a/Mouseion/post-office/puppet.ts b/MouseionArtifacts/post-office/puppet.ts similarity index 100% rename from Mouseion/post-office/puppet.ts rename to MouseionArtifacts/post-office/puppet.ts diff --git a/Mouseion/post-office/puppeteer.ts b/MouseionArtifacts/post-office/puppeteer.ts similarity index 100% rename from Mouseion/post-office/puppeteer.ts rename to MouseionArtifacts/post-office/puppeteer.ts diff --git a/Mouseion/post-office/types.ts b/MouseionArtifacts/post-office/types.ts similarity index 100% rename from Mouseion/post-office/types.ts rename to MouseionArtifacts/post-office/types.ts diff --git a/Mouseion/queues/MessageQueue.ts b/MouseionArtifacts/queues/MessageQueue.ts similarity index 100% rename from Mouseion/queues/MessageQueue.ts rename to MouseionArtifacts/queues/MessageQueue.ts diff --git a/Mouseion/queues/board-rules.ts b/MouseionArtifacts/queues/board-rules.ts similarity index 100% rename from Mouseion/queues/board-rules.ts rename to MouseionArtifacts/queues/board-rules.ts diff --git a/Mouseion/queues/contacts.ts b/MouseionArtifacts/queues/contacts.ts similarity index 100% rename from Mouseion/queues/contacts.ts rename to MouseionArtifacts/queues/contacts.ts diff --git a/Mouseion/utils/cleaner.ts b/MouseionArtifacts/utils/cleaner.ts similarity index 100% rename from Mouseion/utils/cleaner.ts rename to MouseionArtifacts/utils/cleaner.ts diff --git a/Mouseion/utils/common-actions.ts b/MouseionArtifacts/utils/common-actions.ts similarity index 100% rename from Mouseion/utils/common-actions.ts rename to MouseionArtifacts/utils/common-actions.ts diff --git a/Mouseion/utils/do-in-batch.ts b/MouseionArtifacts/utils/do-in-batch.ts similarity index 100% rename from Mouseion/utils/do-in-batch.ts rename to MouseionArtifacts/utils/do-in-batch.ts diff --git a/Mouseion/utils/marionette.ts b/MouseionArtifacts/utils/marionette.ts similarity index 100% rename from Mouseion/utils/marionette.ts rename to MouseionArtifacts/utils/marionette.ts diff --git a/Mouseion/utils/promise-lock.ts b/MouseionArtifacts/utils/promise-lock.ts similarity index 100% rename from Mouseion/utils/promise-lock.ts rename to MouseionArtifacts/utils/promise-lock.ts diff --git a/Mouseion/utils/retry.ts b/MouseionArtifacts/utils/retry.ts similarity index 100% rename from Mouseion/utils/retry.ts rename to MouseionArtifacts/utils/retry.ts diff --git a/Mouseion/utils/sequence.ts b/MouseionArtifacts/utils/sequence.ts similarity index 100% rename from Mouseion/utils/sequence.ts rename to MouseionArtifacts/utils/sequence.ts diff --git a/Mouseion/utils/types.ts b/MouseionArtifacts/utils/types.ts similarity index 100% rename from Mouseion/utils/types.ts rename to MouseionArtifacts/utils/types.ts diff --git a/Pantheon/cli.ts b/Pantheon/cli.ts new file mode 100644 index 000000000..c520d865a --- /dev/null +++ b/Pantheon/cli.ts @@ -0,0 +1,49 @@ +import path from "path"; +import { fork } from 'child_process'; +import { DB, migrationEngine, queryEngine } from '@Pantheon/shim'; + +/** Node-specific for developmental scripting */ +export const prismaNodeCLI = async (...command: string[]): Promise => new Promise(s => { + const child = fork( + path.resolve(__dirname, "..", "node_modules/prisma/build/index.js"), + command, + { + env: { + ...process.env, + PRISMA_MIGRATION_ENGINE_BINARY: migrationEngine, + PRISMA_QUERY_ENGINE_LIBRARY: queryEngine, + DATABASE_URL: "file:" + DB + }, + stdio: "inherit" + } + ); + + child.on("error", err => { throw err }) + + child.on("close", (code, signal) => { + s(code ?? 0); + }) +}) + +/** Shimmed for use with Electron */ +export default async (...command: string[]): Promise => new Promise(s => { + const child = fork( + path.resolve(__dirname, "..", "..", "node_modules/prisma/build/index.js"), + command, + { + env: { + ...process.env, + PRISMA_MIGRATION_ENGINE_BINARY: migrationEngine, + PRISMA_QUERY_ENGINE_LIBRARY: queryEngine, + DATABASE_URL: "file:" + DB + }, + stdio: "inherit" + } + ); + + child.on("error", err => { throw err }) + + child.on("close", (code, signal) => { + s(code ?? 0); + }) +}) \ No newline at end of file diff --git a/Pantheon/client.ts b/Pantheon/client.ts new file mode 100644 index 000000000..46431d4d7 --- /dev/null +++ b/Pantheon/client.ts @@ -0,0 +1,18 @@ +import path from 'path'; +import replace from "replace-in-file" +replace.sync({ + files: path.join(__dirname, "src", "generated", "client", "index.js"), + from: "findSync(process.cwd()", + to: `findSync(require('electron').app.getAppPath()`, +}) +import { PrismaClient } from "@prisma/client" +import { queryEngine } from '@Pantheon/shim'; + +export default new PrismaClient({ + // @ts-ignore + __internal: { + engine: { + binaryPath: queryEngine + } + } +}) diff --git a/Pantheon/prisma/enums.ts b/Pantheon/prisma/enums.ts new file mode 100644 index 000000000..dfc9d76f1 --- /dev/null +++ b/Pantheon/prisma/enums.ts @@ -0,0 +1,26 @@ +//? Constraints because SQLite doesn't support enums + +export enum MailboxProvider { + MICROSOFT="microsoft", + GOOGLE="google" +} + +export enum FolderSpecialty { + INBOX="inbox", + SENT="sent", + STARRED="starred", + SPAM="spam", + DRAFTS="drafts", + ARCHIVE="archive", + TRASH="trash", + BOARD="board", + FOLDER="folder", +} + +export enum BoardRuleActionType { + Star="star", + Forward="forward", + Move="move", + Delete="delete", + Archive="archive" +} diff --git a/Pantheon/prisma/migrations/20230206043535_initialize_db/migration.sql b/Pantheon/prisma/migrations/20230206043535_initialize_db/migration.sql new file mode 100644 index 000000000..ea1b5191a --- /dev/null +++ b/Pantheon/prisma/migrations/20230206043535_initialize_db/migration.sql @@ -0,0 +1,116 @@ +-- CreateTable +CREATE TABLE "Mailbox" ( + "email" TEXT NOT NULL PRIMARY KEY, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "provider" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Message" ( + "mid" TEXT NOT NULL PRIMARY KEY, + "tid" TEXT NOT NULL, + "seen" BOOLEAN NOT NULL, + "starred" BOOLEAN NOT NULL, + "subject" TEXT NOT NULL, + "fromEmail" TEXT NOT NULL, + "timestamp" DATETIME NOT NULL, + CONSTRAINT "Message_fromEmail_fkey" FOREIGN KEY ("fromEmail") REFERENCES "Contact" ("email") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Contact" ( + "name" TEXT NOT NULL, + "email" TEXT NOT NULL PRIMARY KEY, + "base" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "Folder" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "path" TEXT NOT NULL, + "mailboxEmail" TEXT NOT NULL, + CONSTRAINT "Folder_mailboxEmail_fkey" FOREIGN KEY ("mailboxEmail") REFERENCES "Mailbox" ("email") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_to" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_to_A_fkey" FOREIGN KEY ("A") REFERENCES "Contact" ("email") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_to_B_fkey" FOREIGN KEY ("B") REFERENCES "Message" ("mid") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_cc" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_cc_A_fkey" FOREIGN KEY ("A") REFERENCES "Contact" ("email") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_cc_B_fkey" FOREIGN KEY ("B") REFERENCES "Message" ("mid") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_bcc" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_bcc_A_fkey" FOREIGN KEY ("A") REFERENCES "Contact" ("email") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_bcc_B_fkey" FOREIGN KEY ("B") REFERENCES "Message" ("mid") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_recipients" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_recipients_A_fkey" FOREIGN KEY ("A") REFERENCES "Contact" ("email") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_recipients_B_fkey" FOREIGN KEY ("B") REFERENCES "Message" ("mid") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_FolderToMessage" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_FolderToMessage_A_fkey" FOREIGN KEY ("A") REFERENCES "Folder" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_FolderToMessage_B_fkey" FOREIGN KEY ("B") REFERENCES "Message" ("mid") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Mailbox_email_key" ON "Mailbox"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Message_mid_key" ON "Message"("mid"); + +-- CreateIndex +CREATE UNIQUE INDEX "Contact_email_key" ON "Contact"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "Folder_id_key" ON "Folder"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "_to_AB_unique" ON "_to"("A", "B"); + +-- CreateIndex +CREATE INDEX "_to_B_index" ON "_to"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_cc_AB_unique" ON "_cc"("A", "B"); + +-- CreateIndex +CREATE INDEX "_cc_B_index" ON "_cc"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_bcc_AB_unique" ON "_bcc"("A", "B"); + +-- CreateIndex +CREATE INDEX "_bcc_B_index" ON "_bcc"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_recipients_AB_unique" ON "_recipients"("A", "B"); + +-- CreateIndex +CREATE INDEX "_recipients_B_index" ON "_recipients"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_FolderToMessage_AB_unique" ON "_FolderToMessage"("A", "B"); + +-- CreateIndex +CREATE INDEX "_FolderToMessage_B_index" ON "_FolderToMessage"("B"); diff --git a/Pantheon/prisma/migrations/20230207002508_migrate_remaining_types_from_ne_db_and_mouseion/migration.sql b/Pantheon/prisma/migrations/20230207002508_migrate_remaining_types_from_ne_db_and_mouseion/migration.sql new file mode 100644 index 000000000..e62e7726c --- /dev/null +++ b/Pantheon/prisma/migrations/20230207002508_migrate_remaining_types_from_ne_db_and_mouseion/migration.sql @@ -0,0 +1,125 @@ +/* + Warnings: + + - You are about to drop the column `mailboxEmail` on the `Folder` table. All the data in the column will be lost. + - Added the required column `email` to the `Folder` table without a default value. This is not possible if the table is not empty. + - Added the required column `summary` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- CreateTable +CREATE TABLE "Thread" ( + "tid" TEXT NOT NULL PRIMARY KEY, + "timestamp" DATETIME NOT NULL, + "cursor" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "folderId" TEXT NOT NULL, + CONSTRAINT "Thread_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Space" ( + "name" TEXT NOT NULL, + "id" TEXT NOT NULL PRIMARY KEY +); + +-- CreateTable +CREATE TABLE "BoardRule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "isFrom" TEXT, + "isTo" TEXT, + "subjectContains" TEXT, + "bodyContains" TEXT, + "detectedQuickAction" TEXT, + "isSubscription" BOOLEAN, + "attachmentsNameContains" TEXT, + "attachmentsTypeIs" TEXT +); + +-- CreateTable +CREATE TABLE "BoardRuleAction" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "target" TEXT NOT NULL, + "ruleId" TEXT NOT NULL, + CONSTRAINT "BoardRuleAction_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "BoardRule" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_ContactToThread" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_ContactToThread_A_fkey" FOREIGN KEY ("A") REFERENCES "Contact" ("email") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_ContactToThread_B_fkey" FOREIGN KEY ("B") REFERENCES "Thread" ("tid") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_FolderToSpace" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_FolderToSpace_A_fkey" FOREIGN KEY ("A") REFERENCES "Folder" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_FolderToSpace_B_fkey" FOREIGN KEY ("B") REFERENCES "Space" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Folder" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "path" TEXT NOT NULL, + "special" TEXT, + "email" TEXT NOT NULL, + CONSTRAINT "Folder_email_fkey" FOREIGN KEY ("email") REFERENCES "Mailbox" ("email") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Folder" ("id", "name", "path") SELECT "id", "name", "path" FROM "Folder"; +DROP TABLE "Folder"; +ALTER TABLE "new_Folder" RENAME TO "Folder"; +CREATE UNIQUE INDEX "Folder_id_key" ON "Folder"("id"); +CREATE TABLE "new_Message" ( + "mid" TEXT NOT NULL PRIMARY KEY, + "tid" TEXT NOT NULL, + "seen" BOOLEAN NOT NULL, + "starred" BOOLEAN NOT NULL, + "subject" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "timestamp" DATETIME NOT NULL, + "fromEmail" TEXT NOT NULL, + CONSTRAINT "Message_tid_fkey" FOREIGN KEY ("tid") REFERENCES "Thread" ("tid") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Message_fromEmail_fkey" FOREIGN KEY ("fromEmail") REFERENCES "Contact" ("email") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Message" ("fromEmail", "mid", "seen", "starred", "subject", "tid", "timestamp") SELECT "fromEmail", "mid", "seen", "starred", "subject", "tid", "timestamp" FROM "Message"; +DROP TABLE "Message"; +ALTER TABLE "new_Message" RENAME TO "Message"; +CREATE UNIQUE INDEX "Message_mid_key" ON "Message"("mid"); +CREATE TABLE "new_Contact" ( + "name" TEXT NOT NULL, + "email" TEXT NOT NULL PRIMARY KEY, + "base" TEXT NOT NULL, + "messagesSent" INTEGER NOT NULL DEFAULT 0, + "messagesReceived" INTEGER NOT NULL DEFAULT 0, + "blocked" BOOLEAN NOT NULL DEFAULT false, + "rollup" BOOLEAN NOT NULL DEFAULT false, + "whitelisted" BOOLEAN NOT NULL DEFAULT false +); +INSERT INTO "new_Contact" ("base", "email", "name") SELECT "base", "email", "name" FROM "Contact"; +DROP TABLE "Contact"; +ALTER TABLE "new_Contact" RENAME TO "Contact"; +CREATE UNIQUE INDEX "Contact_email_key" ON "Contact"("email"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE UNIQUE INDEX "Thread_tid_key" ON "Thread"("tid"); + +-- CreateIndex +CREATE UNIQUE INDEX "Space_id_key" ON "Space"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ContactToThread_AB_unique" ON "_ContactToThread"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ContactToThread_B_index" ON "_ContactToThread"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_FolderToSpace_AB_unique" ON "_FolderToSpace"("A", "B"); + +-- CreateIndex +CREATE INDEX "_FolderToSpace_B_index" ON "_FolderToSpace"("B"); diff --git a/Pantheon/prisma/migrations/20230305225308_additional_types/migration.sql b/Pantheon/prisma/migrations/20230305225308_additional_types/migration.sql new file mode 100644 index 000000000..69d6ecc00 --- /dev/null +++ b/Pantheon/prisma/migrations/20230305225308_additional_types/migration.sql @@ -0,0 +1,31 @@ +-- CreateTable +CREATE TABLE "Attachment" ( + "path" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "contentType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "timestamp" DATETIME NOT NULL, + "cid" TEXT, + "embedded" BOOLEAN NOT NULL DEFAULT false, + "mid" TEXT NOT NULL, + "authorEmail" TEXT NOT NULL, + CONSTRAINT "Attachment_mid_fkey" FOREIGN KEY ("mid") REFERENCES "Message" ("mid") ON DELETE RESTRICT ON UPDATE CASCADE, + CONSTRAINT "Attachment_authorEmail_fkey" FOREIGN KEY ("authorEmail") REFERENCES "Contact" ("email") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "_ContactToMailbox" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_ContactToMailbox_A_fkey" FOREIGN KEY ("A") REFERENCES "Contact" ("email") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_ContactToMailbox_B_fkey" FOREIGN KEY ("B") REFERENCES "Mailbox" ("email") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Attachment_path_key" ON "Attachment"("path"); + +-- CreateIndex +CREATE UNIQUE INDEX "_ContactToMailbox_AB_unique" ON "_ContactToMailbox"("A", "B"); + +-- CreateIndex +CREATE INDEX "_ContactToMailbox_B_index" ON "_ContactToMailbox"("B"); diff --git a/Pantheon/prisma/migrations/20230305231845_malleability/migration.sql b/Pantheon/prisma/migrations/20230305231845_malleability/migration.sql new file mode 100644 index 000000000..7fddd2582 --- /dev/null +++ b/Pantheon/prisma/migrations/20230305231845_malleability/migration.sql @@ -0,0 +1,93 @@ +/* + Warnings: + + - You are about to drop the `_FolderToSpace` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the column `detectedQuickAction` on the `BoardRule` table. All the data in the column will be lost. + - You are about to drop the column `special` on the `Folder` table. All the data in the column will be lost. + - You are about to drop the column `target` on the `BoardRuleAction` table. All the data in the column will be lost. + +*/ +-- DropIndex +DROP INDEX "Attachment_path_key"; + +-- DropIndex +DROP INDEX "Contact_email_key"; + +-- DropIndex +DROP INDEX "Mailbox_email_key"; + +-- DropIndex +DROP INDEX "Message_mid_key"; + +-- DropIndex +DROP INDEX "Space_id_key"; + +-- DropIndex +DROP INDEX "Thread_tid_key"; + +-- DropIndex +DROP INDEX "_FolderToSpace_B_index"; + +-- DropIndex +DROP INDEX "_FolderToSpace_AB_unique"; + +-- DropTable +PRAGMA foreign_keys=off; +DROP TABLE "_FolderToSpace"; +PRAGMA foreign_keys=on; + +-- CreateTable +CREATE TABLE "_boards" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + CONSTRAINT "_boards_A_fkey" FOREIGN KEY ("A") REFERENCES "Folder" ("id") ON DELETE CASCADE ON UPDATE CASCADE, + CONSTRAINT "_boards_B_fkey" FOREIGN KEY ("B") REFERENCES "Space" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_BoardRule" ( + "id" TEXT NOT NULL PRIMARY KEY, + "isFrom" TEXT, + "isTo" TEXT, + "subjectContains" TEXT, + "bodyContains" TEXT, + "intent" TEXT, + "isSubscription" BOOLEAN, + "attachmentsNameContains" TEXT, + "attachmentsTypeIs" TEXT +); +INSERT INTO "new_BoardRule" ("attachmentsNameContains", "attachmentsTypeIs", "bodyContains", "id", "isFrom", "isSubscription", "isTo", "subjectContains") SELECT "attachmentsNameContains", "attachmentsTypeIs", "bodyContains", "id", "isFrom", "isSubscription", "isTo", "subjectContains" FROM "BoardRule"; +DROP TABLE "BoardRule"; +ALTER TABLE "new_BoardRule" RENAME TO "BoardRule"; +CREATE TABLE "new_Folder" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "path" TEXT NOT NULL, + "type" TEXT NOT NULL DEFAULT 'folder', + "email" TEXT NOT NULL, + CONSTRAINT "Folder_email_fkey" FOREIGN KEY ("email") REFERENCES "Mailbox" ("email") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_Folder" ("email", "id", "name", "path") SELECT "email", "id", "name", "path" FROM "Folder"; +DROP TABLE "Folder"; +ALTER TABLE "new_Folder" RENAME TO "Folder"; +CREATE TABLE "new_BoardRuleAction" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "argument" TEXT, + "targetId" TEXT, + "ruleId" TEXT NOT NULL, + CONSTRAINT "BoardRuleAction_targetId_fkey" FOREIGN KEY ("targetId") REFERENCES "Folder" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "BoardRuleAction_ruleId_fkey" FOREIGN KEY ("ruleId") REFERENCES "BoardRule" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_BoardRuleAction" ("id", "ruleId", "type") SELECT "id", "ruleId", "type" FROM "BoardRuleAction"; +DROP TABLE "BoardRuleAction"; +ALTER TABLE "new_BoardRuleAction" RENAME TO "BoardRuleAction"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE UNIQUE INDEX "_boards_AB_unique" ON "_boards"("A", "B"); + +-- CreateIndex +CREATE INDEX "_boards_B_index" ON "_boards"("B"); diff --git a/Pantheon/prisma/migrations/migration_lock.toml b/Pantheon/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..e5e5c4705 --- /dev/null +++ b/Pantheon/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/Pantheon/prisma/schema.prisma b/Pantheon/prisma/schema.prisma new file mode 100644 index 000000000..69ad68c34 --- /dev/null +++ b/Pantheon/prisma/schema.prisma @@ -0,0 +1,131 @@ +generator client { + provider = "prisma-client-js" + previewFeatures = ["metrics"] +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Mailbox { + email String @id + createdAt DateTime @default(now()) + provider String //* see: enums.ts + folders Folder[] @relation + contacts Contact[] @relation +} + +model Message { + // Identifiers + mid String @id + tid String + // Metadata + seen Boolean + starred Boolean + subject String + summary String + timestamp DateTime + // Thread + thread Thread @relation(fields: [tid], references: [tid]) + // Relations + fromEmail String + from Contact @relation("from", fields: [fromEmail], references: [email]) + to Contact[] @relation("to") + cc Contact[] @relation("cc") + bcc Contact[] @relation("bcc") + allRecipients Contact[] @relation("recipients") + folders Folder[] @relation + attachments Attachment[] @relation +} + +model Attachment { + path String @id + name String + contentType String + size Int + timestamp DateTime + cid String? + embedded Boolean @default(false) + mid String + message Message @relation(fields: [mid], references: [mid]) + authorEmail String + author Contact @relation(fields: [authorEmail], references: [email]) +} + +model Contact { + name String + email String @id + base String + // Stats + messagesSent Int @default(0) + messagesReceived Int @default(0) + // Controls + blocked Boolean @default(false) + rollup Boolean @default(false) + whitelisted Boolean @default(false) + // Relations + mailboxes Mailbox[] @relation + sent Message[] @relation("from") + directRecipientOf Message[] @relation("to") + ccedOn Message[] @relation("cc") + bccedOn Message[] @relation("bcc") + recipientOf Message[] @relation("recipients") + participantOf Thread[] @relation + attachments Attachment[] @relation +} + +model Thread { + tid String @id + // Metadata + timestamp DateTime + cursor DateTime @default(now()) // not y2k safe + // Relations + messages Message[] @relation + folder Folder @relation(fields: [folderId], references: [id]) + folderId String //* pick one, update every time you add a message + participants Contact[] @relation +} + +model Folder { + id String @id @default(uuid()) + name String + path String + type String @default("folder") //* see: enums.ts + email String + mailbox Mailbox @relation(fields: [email], references: [email]) + messages Message[] @relation + threads Thread[] @relation + spaces Space[] @relation("boards") + targetOf BoardRuleAction[] @relation("actsOnBoard") +} + +model Space { + name String + id String @id @default(uuid()) + boards Folder[] @relation("boards") +} + +model BoardRule { + id String @id @default(uuid()) + actions BoardRuleAction[] @relation + // conditions + isFrom String? + isTo String? + subjectContains String? + bodyContains String? + intent String? + isSubscription Boolean? + attachmentsNameContains String? + attachmentsTypeIs String? +} + +model BoardRuleAction { + id String @id @default(uuid()) + type String //* see enums + argument String? + targetId String? + targets Folder? @relation("actsOnBoard", fields: [targetId], references: [id]) + ruleId String + rule BoardRule @relation(fields: [ruleId], references: [id]) +} diff --git a/Pantheon/scripts/prisma-generate.ts b/Pantheon/scripts/prisma-generate.ts new file mode 100644 index 000000000..6409f5bce --- /dev/null +++ b/Pantheon/scripts/prisma-generate.ts @@ -0,0 +1,3 @@ +import { prismaNodeCLI } from "@Pantheon/cli"; + +prismaNodeCLI('generate', '--schema=./Pantheon/prisma/schema.prisma') diff --git a/Pantheon/scripts/prisma-migrate.ts b/Pantheon/scripts/prisma-migrate.ts new file mode 100644 index 000000000..e4c64e9f9 --- /dev/null +++ b/Pantheon/scripts/prisma-migrate.ts @@ -0,0 +1,3 @@ +import { prismaNodeCLI } from "@Pantheon/cli"; + +prismaNodeCLI('migrate', 'dev', '--schema=./Pantheon/prisma/schema.prisma'); diff --git a/Pantheon/scripts/prisma-studio.ts b/Pantheon/scripts/prisma-studio.ts new file mode 100644 index 000000000..ccc6705ca --- /dev/null +++ b/Pantheon/scripts/prisma-studio.ts @@ -0,0 +1,3 @@ +import { prismaNodeCLI } from "@Pantheon/cli"; + +prismaNodeCLI('studio', '--schema=./Pantheon/prisma/schema.prisma'); diff --git a/Pantheon/shim.ts b/Pantheon/shim.ts new file mode 100644 index 000000000..8972bb41b --- /dev/null +++ b/Pantheon/shim.ts @@ -0,0 +1,33 @@ +import path from "path" +import { app } from 'electron'; +import datapath from "@Iris/common/datapath"; + + +const fp = (p: string) => path.join( + app ? app.getAppPath().replace('app.asar', '') : `${__dirname}/../`, + p +) +const executables: Record = { + win32: { + migrationEngine: fp('node_modules/@prisma/engines/migration-engine-windows.exe'), + queryEngine: fp('node_modules/@prisma/engines/query_engine-windows.dll.node') + }, + linux: { + migrationEngine: fp('node_modules/@prisma/engines/migration-engine-debian-openssl-1.1.x'), + queryEngine: fp('node_modules/@prisma/engines/libquery_engine-debian-openssl-1.1.x.so.node') + }, + darwin: { + migrationEngine: fp('node_modules/@prisma/engines/migration-engine-darwin'), + queryEngine: fp('node_modules/@prisma/engines/libquery_engine-darwin.dylib.node') + }, + darwinArm64: { + migrationEngine: fp('node_modules/@prisma/engines/migration-engine-darwin-arm64'), + queryEngine: fp('node_modules/@prisma/engines/libquery_engine-darwin-arm64.dylib.node') + } +} + +export const { migrationEngine, queryEngine } = (process.platform == 'darwin' && process.arch == 'arm64') ? + executables.darwinArm64 + : executables[process.platform] +; +export const DB = datapath("pantheon.db") diff --git a/Veil/App.vue b/Veil/App.vue index 00fb6065e..fb72e299e 100644 --- a/Veil/App.vue +++ b/Veil/App.vue @@ -1,11 +1,10 @@ \ No newline at end of file + + + + + + +@Veil/state/common@Veil/state/common@Veil/state/common \ No newline at end of file diff --git a/Veil/assets/animations/record.json b/Veil/assets/animations/record.json new file mode 100644 index 000000000..fb27d25fd --- /dev/null +++ b/Veil/assets/animations/record.json @@ -0,0 +1 @@ +{"v":"5.5.2","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":488,"h":488,"nm":"recording spectrum_MAIN","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[360.5,249,0],"ix":2},"a":{"a":0,"k":[-73,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[30,163]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[30,45]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[30,104]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[30,42]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[30,83]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[30,37]},{"t":60.0000024438501,"s":[30,100]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":50,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-73.472,8.472],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[300.5,249,0],"ix":2},"a":{"a":0,"k":[-73,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[30,63]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[30,211]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[30,128]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[30,160]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[30,126]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[30,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[30,83]},{"t":60.0000024438501,"s":[30,63]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":50,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-73.472,8.472],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,63],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[240.5,249,0],"ix":2},"a":{"a":0,"k":[-73,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[30,63]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[30,155]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[30,91]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[30,186]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[30,256]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[30,209]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[30,124]},{"t":60.0000024438501,"s":[30,63]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":50,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-73.472,8.472],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,63],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[180.5,249,0],"ix":2},"a":{"a":0,"k":[-73,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[30,63]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[30,83]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[30,154]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[30,126]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[30,160]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[30,128]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[30,211]},{"t":60.0000024438501,"s":[30,63]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":50,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-73.472,8.472],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[30,63],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[120.5,249,0],"ix":2},"a":{"a":0,"k":[-73,8.5,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":0,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":5,"s":[30,37]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":10,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":15,"s":[30,83]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":20,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":25,"s":[30,42]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":30,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":35,"s":[30,104]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":40,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":45,"s":[30,45]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":50,"s":[30,100]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"t":55,"s":[30,163]},{"t":60.0000024438501,"s":[30,100]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"r":{"a":0,"k":50,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":0,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-73.472,8.472],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Rectangle 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":60.0000024438501,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Veil/assets/animations/wave.json b/Veil/assets/animations/wave.json new file mode 100644 index 000000000..ff5ddb982 --- /dev/null +++ b/Veil/assets/animations/wave.json @@ -0,0 +1 @@ +{"v":"5.5.9","fr":29.9700012207031,"ip":0,"op":239.00000973467,"w":1920,"h":1080,"nm":"Main 4","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":0,"nm":"base dots","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[480,540,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"w":1920,"h":1080,"ip":0,"op":239.00000973467,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"base dots","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1440,540,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[50,50,100],"ix":6}},"ao":0,"w":1920,"h":1080,"ip":0,"op":239.00000973467,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 3","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[1002,486,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":239.00000973467,"st":10.0000004073083,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Main Dot 49","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":2,"s":[1922.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":121,"s":[1922.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":240,"s":[1922.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":359,"s":[1922.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":478.000019469339,"s":[1922.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":2,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":121,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":240,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":359,"s":[10,10,100]},{"t":478.000019469339,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":479.00001951007,"st":240.0000097754,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Main Dot 48","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-8,"s":[1842.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":111,"s":[1842.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":230,"s":[1842.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":349,"s":[1842.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":468.000019062031,"s":[1842.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-8,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":111,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":230,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":349,"s":[10,10,100]},{"t":468.000019062031,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":469.000019102762,"st":230.000009368092,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Main Dot 47","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-18,"s":[1762.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":101,"s":[1762.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":220,"s":[1762.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":339,"s":[1762.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":458.000018654722,"s":[1762.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-18,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":101,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":220,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":339,"s":[10,10,100]},{"t":458.000018654722,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":459.000018695453,"st":220.000008960784,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Main Dot 46","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-28,"s":[1682.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":91,"s":[1682.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":210,"s":[1682.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":329,"s":[1682.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":448.000018247414,"s":[1682.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-28,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":91,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":210,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":329,"s":[10,10,100]},{"t":448.000018247414,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":449.000018288145,"st":210.000008553475,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Main Dot 45","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-38,"s":[1602.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":81,"s":[1602.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":200,"s":[1602.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":319,"s":[1602.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":438.000017840106,"s":[1602.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-38,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":81,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":200,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":319,"s":[10,10,100]},{"t":438.000017840106,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":439.000017880837,"st":200.000008146167,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Main Dot 44","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-48,"s":[1522.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":71,"s":[1522.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":190,"s":[1522.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":309,"s":[1522.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":428.000017432797,"s":[1522.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-48,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":71,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":190,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":309,"s":[10,10,100]},{"t":428.000017432797,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":429.000017473528,"st":190.000007738859,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Main Dot 43","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-58,"s":[1442.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":61,"s":[1442.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":180,"s":[1442.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":299,"s":[1442.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":418.000017025489,"s":[1442.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-58,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":61,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":180,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":299,"s":[10,10,100]},{"t":418.000017025489,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":419.00001706622,"st":180.00000733155,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Main Dot 42","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-68,"s":[1362.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":51,"s":[1362.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":170,"s":[1362.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":289,"s":[1362.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":408.000016618181,"s":[1362.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-68,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":51,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":170,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":289,"s":[10,10,100]},{"t":408.000016618181,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":409.000016658911,"st":170.000006924242,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Main Dot 41","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-78,"s":[1282.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41,"s":[1282.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":160,"s":[1282.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":279,"s":[1282.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":398.000016210872,"s":[1282.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-78,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":41,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":160,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":279,"s":[10,10,100]},{"t":398.000016210872,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":399.000016251603,"st":160.000006516934,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Main Dot 40","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-88,"s":[1202.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":31,"s":[1202.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":150,"s":[1202.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":269,"s":[1202.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":388.000015803564,"s":[1202.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-88,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":31,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":150,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":269,"s":[10,10,100]},{"t":388.000015803564,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":389.000015844295,"st":150.000006109625,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"Main Dot 39","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-98,"s":[1122.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":21,"s":[1122.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":140,"s":[1122.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":259,"s":[1122.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":378.000015396256,"s":[1122.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-98,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":21,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":140,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":259,"s":[10,10,100]},{"t":378.000015396256,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":379.000015436986,"st":140.000005702317,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"Main Dot 38","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-108,"s":[1042.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":11,"s":[1042.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":130,"s":[1042.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":249,"s":[1042.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":368.000014988947,"s":[1042.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-108,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":11,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":130,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":249,"s":[10,10,100]},{"t":368.000014988947,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":369.000015029678,"st":130.000005295009,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"Main Dot 37","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-118,"s":[962.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":1,"s":[962.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":120,"s":[962.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":239,"s":[962.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":358.000014581639,"s":[962.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-118,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":1,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":120,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":239,"s":[10,10,100]},{"t":358.000014581639,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":359.00001462237,"st":120.0000048877,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Main Dot 36","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-128,"s":[882.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-9,"s":[882.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":110,"s":[882.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":229,"s":[882.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":348.000014174331,"s":[882.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-128,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-9,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":110,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":229,"s":[10,10,100]},{"t":348.000014174331,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":349.000014215061,"st":110.000004480392,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"Main Dot 35","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-138,"s":[802.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-19,"s":[802.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":100,"s":[802.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":219,"s":[802.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":338.000013767022,"s":[802.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-138,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-19,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":100,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":219,"s":[10,10,100]},{"t":338.000013767022,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":339.000013807753,"st":100.000004073084,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Main Dot 34","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-148,"s":[722.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-29,"s":[722.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":90,"s":[722.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":209,"s":[722.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":328.000013359714,"s":[722.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-148,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-29,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":90,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":209,"s":[10,10,100]},{"t":328.000013359714,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":329.000013400445,"st":90.0000036657751,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"Main Dot 33","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-158,"s":[642.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-39,"s":[642.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":80,"s":[642.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":199,"s":[642.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":318.000012952406,"s":[642.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-158,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-39,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":80,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":199,"s":[10,10,100]},{"t":318.000012952406,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":319.000012993136,"st":80.0000032584668,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"Main Dot 32","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-168,"s":[562.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-49,"s":[562.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":70,"s":[562.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":189,"s":[562.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":308.000012545097,"s":[562.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-168,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-49,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":70,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":189,"s":[10,10,100]},{"t":308.000012545097,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":309.000012585828,"st":70.0000028511585,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"Main Dot 31","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-178,"s":[482.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-59,"s":[482.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":60,"s":[482.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":179,"s":[482.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":298.000012137789,"s":[482.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-178,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-59,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":60,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":179,"s":[10,10,100]},{"t":298.000012137789,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":299.00001217852,"st":60.0000024438501,"bm":0},{"ddd":0,"ind":21,"ty":4,"nm":"Main Dot 30","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-188,"s":[402.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-69,"s":[402.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":50,"s":[402.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":169,"s":[402.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":288.00001173048,"s":[402.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-188,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-69,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":50,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":169,"s":[10,10,100]},{"t":288.00001173048,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":289.000011771211,"st":50.0000020365418,"bm":0},{"ddd":0,"ind":22,"ty":4,"nm":"Main Dot 29","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-198,"s":[322.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-79,"s":[322.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":40,"s":[322.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":159,"s":[322.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":278.000011323172,"s":[322.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-198,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-79,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":40,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":159,"s":[10,10,100]},{"t":278.000011323172,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":279.000011363903,"st":40.0000016292334,"bm":0},{"ddd":0,"ind":23,"ty":4,"nm":"Main Dot 28","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-208,"s":[242.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-89,"s":[242.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":30,"s":[242.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":149,"s":[242.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":268.000010915864,"s":[242.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-208,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-89,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":30,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":149,"s":[10,10,100]},{"t":268.000010915864,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":269.000010956595,"st":30.0000012219251,"bm":0},{"ddd":0,"ind":24,"ty":4,"nm":"Main Dot 27","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-218,"s":[162.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-99,"s":[162.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":20,"s":[162.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":139,"s":[162.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":258.000010508555,"s":[162.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-218,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-99,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":20,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":139,"s":[10,10,100]},{"t":258.000010508555,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":259.000010549286,"st":20.0000008146167,"bm":0},{"ddd":0,"ind":25,"ty":4,"nm":"Main Dot 26","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-228,"s":[82.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-109,"s":[82.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":10,"s":[82.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":129,"s":[82.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":248.000010101247,"s":[82.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-228,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-109,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":10,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":129,"s":[10,10,100]},{"t":248.000010101247,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4,"x":"var $bm_rt;\n$bm_rt = thisComp.layer('Main Dot color').content('Ellipse 1').content('Fill 1').color;"},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":249.000010141978,"st":10.0000004073083,"bm":0},{"ddd":0,"ind":26,"ty":4,"nm":"Main Dot color","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-238,"s":[2.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":-119,"s":[2.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[2.605,510.06,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":119,"s":[2.605,750.06,0],"to":[0,0,0],"ti":[0,0,0]},{"t":238.000009693939,"s":[2.605,510.06,0]}],"ix":2},"a":{"a":0,"k":[-328.531,-12.122,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-238,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":-119,"s":[10,10,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[50,50,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":119,"s":[10,10,100]},{"t":238.000009693939,"s":[50,50,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[50.694,50.694],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.294117647059,0.63137254902,0.929411824544,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[-328.531,-12.122],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":239.00000973467,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,740,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[210,210,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":239.00000973467,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,740,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[210,210,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":21.0000008553475,"st":-218.000008879322,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,740,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[210,210,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":20.0000008146167,"op":250.000010182709,"st":20.0000008146167,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,740,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[200,200,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":41.0000016699642,"st":-198.000008064705,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,740,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[200,200,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":40.0000016292334,"op":250.000010182709,"st":40.0000016292334,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,720,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[190,190,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":61.0000024845809,"st":-178.000007250089,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,720,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[190,190,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":60.0000024438501,"op":250.000010182709,"st":60.0000024438501,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,700,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[180,180,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":81.0000032991976,"st":-158.000006435472,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,700,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[180,180,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":80.0000032584668,"op":250.000010182709,"st":80.0000032584668,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,680,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[170,170,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":101.000004113814,"st":-138.000005620855,"bm":0},{"ddd":0,"ind":11,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,680,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[170,170,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":100.000004073084,"op":250.000010182709,"st":100.000004073084,"bm":0},{"ddd":0,"ind":12,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,660,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[160,160,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.239215701818,0.807843208313,0.956862807274,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":121.000004928431,"st":-118.000004806239,"bm":0},{"ddd":0,"ind":13,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,660,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[160,160,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.239215701818,0.807843208313,0.956862807274,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":120.0000048877,"op":250.000010182709,"st":120.0000048877,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,640,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[150,150,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.239215701818,0.807843208313,0.956862807274,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":101.000004113814,"st":-138.000005620855,"bm":0},{"ddd":0,"ind":15,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,640,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[150,150,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.239215701818,0.807843208313,0.956862807274,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":100.000004073084,"op":250.000010182709,"st":100.000004073084,"bm":0},{"ddd":0,"ind":16,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,620,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[140,140,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.239215701818,0.807843208313,0.956862807274,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":81.0000032991976,"st":-158.000006435472,"bm":0},{"ddd":0,"ind":17,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,620,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[140,140,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.239215701818,0.807843208313,0.956862807274,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":80.0000032584668,"op":250.000010182709,"st":80.0000032584668,"bm":0},{"ddd":0,"ind":18,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,600,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":61.0000024845809,"st":-178.000007250089,"bm":0},{"ddd":0,"ind":19,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,600,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":60.0000024438501,"op":250.000010182709,"st":60.0000024438501,"bm":0},{"ddd":0,"ind":20,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,580,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[120,120,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":41.0000016699642,"st":-198.000008064705,"bm":0},{"ddd":0,"ind":21,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,580,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[120,120,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.29411765933,0.631372570992,0.929411828518,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":40.0000016292334,"op":250.000010182709,"st":40.0000016292334,"bm":0},{"ddd":0,"ind":22,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,560,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[110,110,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":21.0000008553475,"st":-218.000008879322,"bm":0},{"ddd":0,"ind":23,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,560,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[110,110,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":20.0000008146167,"op":250.000010182709,"st":20.0000008146167,"bm":0},{"ddd":0,"ind":24,"ty":0,"nm":"double dots","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,540,0],"ix":2},"a":{"a":0,"k":[960,540,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ef":[{"ty":21,"nm":"Fill","np":9,"mn":"ADBE Fill","ix":1,"en":1,"ef":[{"ty":10,"nm":"Fill Mask","mn":"ADBE Fill-0001","ix":1,"v":{"a":0,"k":0,"ix":1}},{"ty":7,"nm":"All Masks","mn":"ADBE Fill-0007","ix":2,"v":{"a":0,"k":0,"ix":2}},{"ty":2,"nm":"Color","mn":"ADBE Fill-0002","ix":3,"v":{"a":0,"k":[0.345098048449,0.450980424881,0.898039281368,1],"ix":3}},{"ty":7,"nm":"Invert","mn":"ADBE Fill-0006","ix":4,"v":{"a":0,"k":0,"ix":4}},{"ty":0,"nm":"Horizontal Feather","mn":"ADBE Fill-0003","ix":5,"v":{"a":0,"k":0,"ix":5}},{"ty":0,"nm":"Vertical Feather","mn":"ADBE Fill-0004","ix":6,"v":{"a":0,"k":0,"ix":6}},{"ty":0,"nm":"Opacity","mn":"ADBE Fill-0005","ix":7,"v":{"a":0,"k":1,"ix":7}}]}],"w":1920,"h":1080,"ip":0,"op":239.00000973467,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Veil/assets/animations/writing.json b/Veil/assets/animations/writing.json new file mode 100644 index 000000000..35c755960 --- /dev/null +++ b/Veil/assets/animations/writing.json @@ -0,0 +1 @@ +{"v":"5.5.7","meta":{"g":"LottieFiles AE 0.1.20","a":"Monika Madurska","k":"contact, icon, moving, pen","d":"contact icon. Moving pen","tc":""},"fr":25,"ip":0,"op":45,"w":156,"h":156,"nm":"Kontakt","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":2,"ty":4,"nm":"pen Outlines","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":22,"s":[-51.973]},{"t":44,"s":[0]}],"ix":10},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[65.539,53.887,0],"to":[2.409,0,0],"ti":[0,0,0]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0},"t":22,"s":[79.992,53.887,0],"to":[0,0,0],"ti":[2.409,0,0]},{"t":44,"s":[65.539,53.887,0]}],"ix":2},"a":{"a":0,"k":[0.589,15.347,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.084,0],[0.063,0.064],[0,0],[-0.128,0.128],[-0.128,-0.127],[0,0],[0.128,-0.128]],"o":[[-0.083,0],[0,0],[-0.128,-0.127],[0.128,-0.127],[0,0],[0.128,0.127],[-0.063,0.064]],"v":[[0.418,0.761],[0.188,0.665],[-0.649,-0.172],[-0.649,-0.634],[-0.187,-0.634],[0.649,0.203],[0.649,0.665]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.581,1.701],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.084,0],[0.064,0.064],[-0.127,0.127],[0,0],[-0.127,-0.128],[0.128,-0.127],[0,0]],"o":[[-0.084,0],[-0.127,-0.128],[0,0],[0.128,-0.128],[0.128,0.128],[0,0],[-0.064,0.064]],"v":[[-2.822,3.165],[-3.053,3.069],[-3.053,2.607],[2.59,-3.037],[3.053,-3.037],[3.053,-2.575],[-2.592,3.069]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[9.03,3.415],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.084,0],[0.063,0.064],[-0.128,0.127],[0,0],[-0.127,-0.128],[0.128,-0.128],[0,0]],"o":[[-0.083,0],[-0.128,-0.128],[0,0],[0.128,-0.128],[0.128,0.128],[0,0],[-0.063,0.064]],"v":[[-0.281,0.624],[-0.512,0.528],[-0.512,0.066],[0.049,-0.496],[0.512,-0.496],[0.512,-0.033],[-0.051,0.528]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0.889,15.119],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0.827,-1.774],[-1.392,1.392],[0,0]],"o":[[0,0],[-1.392,1.391],[1.775,-0.826],[0,0],[0,0]],"v":[[2.564,-3.981],[-0.488,-0.929],[-3.827,3.837],[0.94,0.498],[3.992,-2.553]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.041,0],[0.063,0.062],[-0.05,0.122],[-1.648,1.648],[0,0],[-0.122,-0.122],[0,0],[0.128,-0.128],[0,0],[2.159,-0.881]],"o":[[-0.085,0],[-0.093,-0.094],[0.881,-2.159],[0,0],[0.122,-0.122],[0,0],[0.128,0.128],[0,0],[-1.65,1.649],[-0.04,0.017]],"v":[[-4.46,4.797],[-4.691,4.702],[-4.762,4.346],[-0.952,-1.392],[2.333,-4.675],[2.794,-4.675],[4.685,-2.785],[4.685,-2.322],[1.403,0.96],[-4.337,4.773]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[5.63,10.384],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":4,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0.394,0.394],[0.393,-0.393]],"o":[[0,0],[0,0],[0.394,-0.393],[-0.394,-0.393],[0,0]],"v":[[-3.009,1.581],[-1.581,3.01],[2.687,-1.26],[2.687,-2.688],[1.26,-2.688]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0.084,0],[0.064,0.064],[0,0],[0,0.087],[-0.062,0.061],[0,0],[-0.648,-0.65],[0.649,-0.649],[0,0]],"o":[[-0.083,0],[0,0],[-0.062,-0.061],[0,-0.087],[0,0],[0.648,-0.65],[0.649,0.648],[0,0],[-0.063,0.064]],"v":[[-1.581,3.799],[-1.812,3.702],[-3.702,1.812],[-3.798,1.581],[-3.702,1.35],[0.797,-3.148],[3.149,-3.148],[3.149,-0.797],[-1.35,3.702]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"mm","mm":1,"nm":"Merge Paths 1","mn":"ADBE Vector Filter - Merge","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[11.665,4.36],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":4,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":247,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"linia Outlines","parent":7,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[65.804,55.247,0],"ix":2},"a":{"a":0,"k":[0.822,0.329,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"hasMask":true,"masksProperties":[{"inv":false,"mode":"a","pt":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0.166,-0.578],[-0.046,-0.564],[-0.046,1.294],[0.166,1.28]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":21,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[15.568,-0.564],[-0.046,-0.564],[-0.046,1.294],[15.568,1.294]],"c":true}]},{"t":44,"s":[{"i":[[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0]],"v":[[0.166,-0.578],[-0.046,-0.564],[-0.046,1.294],[0.166,1.28]],"c":true}]}],"ix":1},"o":{"a":0,"k":100,"ix":3},"x":{"a":0,"k":0,"ix":4},"nm":"Mask 1"}],"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0.181,0],[0,0],[0,0.181],[-0.181,0],[0,0],[0,-0.181]],"o":[[0,0],[-0.181,0],[0,-0.181],[0,0],[0.181,0],[0,0.181]],"v":[[7.183,0.327],[-7.184,0.327],[-7.51,0],[-7.184,-0.327],[7.183,-0.327],[7.51,0]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.2824,0.4353,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[7.759,0.577],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":247,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":3,"nm":"Warstwa 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[109.361,64.556,0],"ix":2},"a":{"a":0,"k":[83.612,44.806,0],"ix":1},"s":{"a":0,"k":[302.711,302.711,100],"ix":6}},"ao":0,"ip":0,"op":247,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/Veil/assets/css/auto.css b/Veil/assets/css/auto.css index dc06c1366..27eb579fc 100644 --- a/Veil/assets/css/auto.css +++ b/Veil/assets/css/auto.css @@ -6,18 +6,15 @@ --primary-color-hover: #4366e8; /* Background colors (Application, elements and windows) */ - --primary-background-color: #1e212a; - --secondary-background-color: #0c0e13; + --primary-background-color: #8b94d424; + --secondary-background-color: #181920db; - --primary-background-color-hover: #14161b; - --secondary-background-color-hover: #21201d; - - --modal-backdrop: #000000b3; + --modal-backdrop: #16171ef2; /* Fonts */ --primary-font: ""; - --primary-font-color: #929aad; - --strong-font-color: #ffffff; + --primary-font-color: #b6b8d6; + --strong-font-color: #dddee9; --body-font-size: 14px; --small-font-size: 13px; @@ -29,55 +26,117 @@ --primary-border-radius: 5px; /* Changes the color fo all icons */ - --icon-filter: invert(66%) sepia(14%) saturate(350%) hue-rotate(184deg) brightness(91%) contrast(88%); + --primary-color-filter: invert(83%) sepia(59%) saturate(1823%) hue-rotate(189deg) brightness(82%) contrast(87%); + --icon-filter: invert(83%) sepia(59%) saturate(1823%) hue-rotate(189deg) brightness(82%) contrast(87%); + ; /* Shadows */ --sidebar-shadow: 3px 50px 10px #00000040; --header-shadow: 8px 1px 10px #0000002e; - --board-shadow: 0 0 15px #00000024; + --board-shadow: 0px 4px 20px 0 #0000001c; --modal-shadow: 0 0 75px -10px #00000047; --email-bottom-shadow: inset 5px 22px 40px -20px #00000073; } +.editor { + background: var(--primary-background-color); +} + +body { + background-color: #00011140; +} + @media screen and (prefers-color-scheme: light) { /* Light theme Variables */ :root { /* Background colors (Application, elements and windows) */ - --primary-background-color: #d6e1f0; - --secondary-background-color: #ffffff; - - --primary-background-color-hover: #bbc9dc; - --secondary-background-color-hover: #21201d; + --primary-background-color: #49494917; + --secondary-background-color: #ffffffc9; - --modal-backdrop: #ffffff7d; + --modal-backdrop: #ffffffb0; /* Fonts */ --primary-font: ""; - --primary-font-color: #515688; - --strong-font-color: #282c4c; + --primary-font-color: #0000007a; + --strong-font-color: #000000e0; /* Changes the color fo all icons */ - --icon-filter: invert(31%) sepia(7%) saturate(3361%) hue-rotate(197deg) brightness(101%) contrast(84%); + --icon-filter: none; /* Shadows */ --sidebar-shadow: none; - --header-shadow: none; - --board-shadow: none; - --modal-shadow: box-shadow: 0 0 75px -10px #00408547; - --modal-special-background: #e1e7f7cc; - --email-bottom-shadow: inset 5px 30px 10px -30px #00000017 !important; + --header-shadow: 8px 1px 10px #0000002e; + --board-shadow: 0px 4px 20px 0 #ffffff5e; + --modal-shadow: 0 0 70px 20px #00000042; + --modal-bg: #1f1f1fcf; + --email-bottom-shadow: inset 5px 22px 40px -20px #00000012; } + #app { overflow: hidden; user-select: none; background-position: top; background-size: cover; } + .modal { background: var(--modal-special-background); } + .email-card:hover { filter: brightness(0.97) !important; } + + .composer .left { + background: var(--secondary-background-color) !important; + } + + .composer .right { + background: var(--primary-background-color) !important; + } + + .composer input { + border: none; + border-top: 2px solid var(--primary-background-color); + background: var(--secondary-background-color) !important; + } + + .composer .textarea { + border-top: 2px solid var(--primary-background-color) !important; + background: var(--secondary-background-color) !important; + } + + .composer input:focus, + .composer input:active { + filter: brightness(0.96) !important; + transition: .1s; + } + + .composer .bottom { + background: var(--secondary-background-color) !important; + } + + .small .board-header:hover { + filter: brightness(0.95) !important; + transition: .1s; + } + + .small-board-click-area:hover+.board-header { + filter: brightness(0.95) !important; + transition: .1s; + } + + .email-card.qr .quick-reply { + background: #fff !important; + box-shadow: inset 0px 0px 30px -10px #00000054 !important; + } + + .editor { + background: var(--secondary-background-color); + } + + body { + background-color: #0005d50a; + } } \ No newline at end of file diff --git a/Veil/assets/css/base.css b/Veil/assets/css/base.css index 8f7b447df..42fd8a16f 100644 --- a/Veil/assets/css/base.css +++ b/Veil/assets/css/base.css @@ -1,8 +1,10 @@ @import'./bootstrap.min.css'; @import "./auto.css"; /* TODO: OTHER CSS FILES TO IMPORT BASED ON PREFERENCE +TODO: NEED TO UPDATE LIGHT AND DARK CSS WITH AUTO PROPERTIES @import "./dark.css"; @import "./light.css"; +@import "./tesla.css"; */ html, @@ -11,15 +13,13 @@ body { height: 100%; margin: 0; padding: 0; - background-color: var(--secondary-background-color); display: inline-flex; } #app { width: 100%; height: 100%; - padding: 28px 0 0 0; - background-color: var(--secondary-background-color); + background-color: transparent; display: inline-flex; } @@ -41,7 +41,7 @@ section { a { text-decoration: none !important; - cursor: pointer !important; + cursor: default !important; } p { @@ -135,7 +135,7 @@ textarea { color: var(--strong-font-color); outline: none; cursor: text; - transition: .2s; + transition: .1s; } .primary { @@ -208,7 +208,7 @@ textarea { .aikocheckbox input[type="checkbox"]+label { position: relative; - cursor: pointer; + cursor: default; } .aikocheckbox input[type="checkbox"]+label:hover:before { @@ -266,7 +266,7 @@ textarea { [type="radio"]:not(:checked)+label { position: relative; padding-left: 28px; - cursor: pointer; + cursor: default; line-height: 20px; display: inline-block; color: var(--primary-font-color); @@ -309,4 +309,26 @@ textarea { opacity: 1; -webkit-transform: scale(1); transform: scale(1); +} + +.ghost.small { + width: 30px !important; + transition: .2s; +} + +.ghost.large { + width: 300px !important; + transition: .2s; +} + +.ghost.medium { + width: 150px !important; + transition: .2s; +} + + +.dragarea { + width: 100%; + height: 100%; + transition: .2s; } \ No newline at end of file diff --git a/Veil/assets/css/composer-exclude.css b/Veil/assets/css/composer-exclude.css new file mode 100644 index 000000000..fe975628c --- /dev/null +++ b/Veil/assets/css/composer-exclude.css @@ -0,0 +1,24 @@ + /* Composer CSS DO NOT SEND WITH EMAILS */ + + .ProseMirror { + outline: none; + padding: 10px; + border-top: 2px solid var(--primary-background-color) !important; + height: 100% !important; + } + + + @media screen and (prefers-color-scheme: dark) { + blockquote { + background: var(--primary-background-color-hover) !important; + border-left: 3px solid var(--primary-font-color) !important; + } + + [type=checkbox]:checked::before { + background-color: #486fff !important; + } + + [type=checkbox]::before { + background-color: #2d3748 !important; + } + } \ No newline at end of file diff --git a/Veil/assets/css/composer.css b/Veil/assets/css/composer.css new file mode 100644 index 000000000..a144e95bf --- /dev/null +++ b/Veil/assets/css/composer.css @@ -0,0 +1,174 @@ + /* Composer CSS to send with emails */ + + ul, + ol { + padding: 0 1rem; + margin-left: 20px; + } + + p { + margin-bottom: 0 !important; + font-size: 15px !important; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + line-height: 1.1; + } + + strong { + font-weight: bold !important; + } + + pre, + code { + background-color: #2d3748; + color: #FFF; + padding: 0.75rem 1rem; + border-radius: 5px; + transition: .2s + } + + pre { + border-left: 3px solid #486fff; + margin: 10px 0 10px 20px !important; + } + + code { + color: inherit; + padding: 0; + background: none; + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + color: #fff !important; + font-size: 16px; + } + + li::marker { + color: #515688; + } + + pre:hover { + opacity: .9; + transition: .1s; + } + + img { + max-width: 100%; + height: auto; + } + + blockquote { + background: #f3f4f8; + border-left: 3px solid #515688; + border-radius: 2px; + font-style: italic; + font-size: 22px; + margin: 10px 0 10px 20px !important; + padding: 15px !important; + transition: .2s + } + + blockquote:hover { + opacity: .9; + transition: .1s; + } + + + hr { + border: none; + border-top: 2px solid rgba(#0D0D0D, 0.1); + margin: 2rem 0; + } + + [data-checked="true"], + [data-checked="false"] { + display: block; + } + + [data-type="taskList"] { + display: inline-block !important; + } + + [data-type="taskList"] div { + display: inline-block; + } + + [data-type="taskList"] label input { + margin-right: 10px; + } + + + + + + + + + /* Basic styling */ + + [type=checkbox] { + width: 25px; + height: 25px; + color: dodgerblue; + vertical-align: middle; + -webkit-appearance: none; + background: none; + border-top: none !important; + outline: 0; + flex-grow: 0; + border-radius: 5px !important; + background-color: #FFFFFF; + transition: background 300ms; + cursor: default; + } + + + /* Pseudo element for check styling */ + + [type=checkbox]::before { + content: ""; + color: transparent; + display: block; + width: inherit; + height: inherit; + border-radius: 5px !important; + border: 0; + background-color: #d6e1f0; + background-size: contain; + box-shadow: inset 0 0 0 1px #CCD3D8; + } + + + /* Checked */ + [type=checkbox]:checked { + background-color: #486fff; + } + + [type=checkbox]:checked::before { + box-shadow: none; + background-color: #486fff; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E %3Cpath d='M15.88 8.29L10 14.17l-1.88-1.88a.996.996 0 1 0-1.41 1.41l2.59 2.59c.39.39 1.02.39 1.41 0L17.3 9.7a.996.996 0 0 0 0-1.41c-.39-.39-1.03-.39-1.42 0z' fill='%23fff'/%3E %3C/svg%3E"); + } + + /* IE */ + [type=checkbox]::-ms-check { + content: ""; + color: transparent; + display: block; + width: inherit; + height: inherit; + border-radius: 5px !important; + border: 0; + background-color: transparent; + background-size: contain; + box-shadow: inset 0 0 0 1px #CCD3D8; + } + + [type=checkbox]:checked::-ms-check { + box-shadow: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E %3Cpath d='M15.88 8.29L10 14.17l-1.88-1.88a.996.996 0 1 0-1.41 1.41l2.59 2.59c.39.39 1.02.39 1.41 0L17.3 9.7a.996.996 0 0 0 0-1.41c-.39-.39-1.03-.39-1.42 0z' fill='%23fff'/%3E %3C/svg%3E"); + } \ No newline at end of file diff --git a/Veil/assets/css/dark.css b/Veil/assets/css/dark.css index 76a306d65..33b8c70a7 100644 --- a/Veil/assets/css/dark.css +++ b/Veil/assets/css/dark.css @@ -1,21 +1,17 @@ -/* Base Variables */ :root { /* Brand color */ --primary-color: #486fff; --primary-color-hover: #4366e8; /* Background colors (Application, elements and windows) */ - --primary-background-color: #1e212a; - --secondary-background-color: #0c0e13; + --primary-background-color: #8b94d417; + --secondary-background-color: #080a188a; - --primary-background-color-hover: #14161b; - --secondary-background-color-hover: #21201d; - - --modal-backdrop: #000000b3; + --modal-backdrop: #0e0e13f0; /* Fonts */ --primary-font: ""; - --primary-font-color: #929aad; + --primary-font-color: #9697c7; --strong-font-color: #ffffff; --body-font-size: 14px; --small-font-size: 13px; @@ -28,12 +24,22 @@ --primary-border-radius: 5px; /* Changes the color fo all icons */ - --icon-filter: invert(66%) sepia(14%) saturate(350%) hue-rotate(184deg) brightness(91%) contrast(88%); + --primary-color-filter: invert(83%) sepia(59%) saturate(1823%) hue-rotate(189deg) brightness(82%) contrast(87%); + --icon-filter: invert(83%) sepia(59%) saturate(1823%) hue-rotate(189deg) brightness(82%) contrast(87%); + ; /* Shadows */ --sidebar-shadow: 3px 50px 10px #00000040; --header-shadow: 8px 1px 10px #0000002e; - --board-shadow: 0 0 15px #00000024; + --board-shadow: 0px 4px 20px 0 #0000001c; --modal-shadow: 0 0 75px -10px #00000047; --email-bottom-shadow: inset 5px 22px 40px -20px #00000073; } + +.editor { + background: var(--primary-background-color); +} + +body { + background-color: #00023f29; +} \ No newline at end of file diff --git a/Veil/assets/css/light.css b/Veil/assets/css/light.css index d751f9cd4..395407612 100644 --- a/Veil/assets/css/light.css +++ b/Veil/assets/css/light.css @@ -2,7 +2,7 @@ :root { /* Background colors (Application, elements and windows) */ --primary-background-color: #d6e1f0; - --secondary-background-color: #ffffff; + --secondary-background-color: rgba(255, 255, 255, 0.2); --primary-background-color-hover: #bbc9dc; --secondary-background-color-hover: #21201d; @@ -25,15 +25,71 @@ --modal-special-background: #e1e7f7cc; --email-bottom-shadow: inset 5px 30px 10px -30px #00000017 !important; } + #app { overflow: hidden; user-select: none; background-position: top; background-size: cover; } + .modal { background: var(--modal-special-background); } + .email-card:hover { filter: brightness(0.97) !important; + } + + .composer-options { + box-shadow: rgb(0 0 0 / 15%) 0px 0px 7px 0px; + width: fit-content; + } + + .composer .left { + background: var(--secondary-background-color) !important; + } + + .composer .right { + background: var(--primary-background-color) !important; + } + + .composer input { + border: none; + border-top: 2px solid var(--primary-background-color); + background: var(--secondary-background-color) !important; + } + + .composer .textarea { + border-top: 2px solid var(--primary-background-color) !important; + background: var(--secondary-background-color) !important; + } + + .composer input:focus, + .composer input:active { + filter: brightness(0.96) !important; + transition: .1s; + } + + .composer .bottom { + background: var(--secondary-background-color) !important; + } + + .small .board-header:hover { + filter: brightness(0.95) !important; + transition: .1s; + } + + .small-board-click-area:hover+.board-header { + filter: brightness(0.95) !important; + transition: .1s; + } + + .email-card.qr .quick-reply { + background: #fff !important; + box-shadow: inset 0px 0px 30px -10px #00000054 !important; + } + + .editor { + background: var(--secondary-background-color); } \ No newline at end of file diff --git a/Veil/assets/css/tesla.css b/Veil/assets/css/tesla.css new file mode 100644 index 000000000..87b55eabb --- /dev/null +++ b/Veil/assets/css/tesla.css @@ -0,0 +1,74 @@ +.sidebar .top a { + height: 60px; + padding-top: 18px !important; + padding-left: 10px; + font-weight: 600; + font-size: 16px; + background-color: var(--primary-background-color); + color: var(--strong-font-color) !important; +} + +.sidebar .top .composecont a img { + margin-top: -2px !important; +} + +.board-width-trigger { + display: none; +} + +.acont { + display: none; +} + +.tesla-warning { + display: block !important; + width: 100%; + text-align: center; + margin: auto; + background: red; + color: #fff; + margin-top: -1px; + padding-top: 2px; + font-size: 18px !important; +} + +.sidebar .top .composecont a { + font-weight: 600; + background-color: var(--primary-color); +} + +.icon-filter { + filter: invert(100%) !important; +} + +.sidebar .top a img { + margin-top: -15px !important; +} + +.normal { + opacity: 1 !important; +} + +.email-card .subject { + font-size: 17px !important; + height: 24px !important; +} + +.email-card .preview { + font-size: 16px !important; + line-height: 19px !important; + height: 39px !important; +} + +.email-card .bottom .quick-action span { + font-size: 14px !important; +} + +.count { + margin-left: 11px !important; + margin-bottom: 22px !important; +} + +.board-header h1 { + font-size: 22px !important; +} \ No newline at end of file diff --git a/Veil/assets/icons/calendly.svg b/Veil/assets/icons/calendly.svg new file mode 100644 index 000000000..e1d833c36 --- /dev/null +++ b/Veil/assets/icons/calendly.svg @@ -0,0 +1,10 @@ + + + + v + Created with Sketch. + + + + + \ No newline at end of file diff --git a/Veil/assets/icons/down.svg b/Veil/assets/icons/down.svg new file mode 100644 index 000000000..bb794dc6c --- /dev/null +++ b/Veil/assets/icons/down.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Veil/assets/icons/logo.svg b/Veil/assets/icons/logo.svg new file mode 100644 index 000000000..2e867217c --- /dev/null +++ b/Veil/assets/icons/logo.svg @@ -0,0 +1,12 @@ + + + + logo-white + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/Veil/assets/icons/roundedx.svg b/Veil/assets/icons/roundedx.svg new file mode 100644 index 000000000..33a682795 --- /dev/null +++ b/Veil/assets/icons/roundedx.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Veil/assets/icons/scribe.svg b/Veil/assets/icons/scribe.svg new file mode 100644 index 000000000..0788064c3 --- /dev/null +++ b/Veil/assets/icons/scribe.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/Veil/assets/icons/start-record.png b/Veil/assets/icons/start-record.png new file mode 100644 index 000000000..cdbba5c0a Binary files /dev/null and b/Veil/assets/icons/start-record.png differ diff --git a/Veil/assets/icons/start-record.svg b/Veil/assets/icons/start-record.svg new file mode 100644 index 000000000..e09302cd3 --- /dev/null +++ b/Veil/assets/icons/start-record.svg @@ -0,0 +1,10 @@ + + + + start-record + Created with Sketch. + + + + + \ No newline at end of file diff --git a/Veil/assets/icons/zoom.svg b/Veil/assets/icons/zoom.svg new file mode 100644 index 000000000..ab9f4232e --- /dev/null +++ b/Veil/assets/icons/zoom.svg @@ -0,0 +1,10 @@ + + + + zoom-logo-png-video-meeting-call-software + Created with Sketch. + + + + + \ No newline at end of file diff --git a/Veil/assets/img/logo.png b/Veil/assets/img/logo.png deleted file mode 100644 index 8536e2c87..000000000 Binary files a/Veil/assets/img/logo.png and /dev/null differ diff --git a/Veil/assets/js/hark.aiko.js b/Veil/assets/js/hark.aiko.js new file mode 100644 index 000000000..ca89ac941 --- /dev/null +++ b/Veil/assets/js/hark.aiko.js @@ -0,0 +1,304 @@ +(function(e){if("function"==typeof bootstrap)bootstrap("hark",e);else if("object"==typeof exports)module.exports=e();else if("function"==typeof define&&define.amd)define(e);else if("undefined"!=typeof ses){if(!ses.ok())return;ses.makeHark=e}else"undefined"!=typeof window?window.hark=e():global.hark=e()})(function(){var define,ses,bootstrap,module,exports; + return (function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s maxVolume && fftBins[i] < 0) { + maxVolume = fftBins[i]; + } + }; + + return maxVolume; + } + + + var audioContextType; + if (typeof window !== 'undefined') { + audioContextType = window.AudioContext || window.webkitAudioContext; + } + // use a single audio context due to hardware limits + var audioContext = null; + module.exports = function(stream, options) { + var harker = new WildEmitter(); + + // make it not break in non-supported browsers + if (!audioContextType) return harker; + + //Config + var options = options || {}, + smoothing = (options.smoothing || 0.1), + interval = (options.interval || 50), + threshold = options.threshold, + play = options.play, + history = options.history || 10, + running = true; + + // Ensure that just a single AudioContext is internally created + audioContext = options.audioContext || audioContext || new audioContextType(); + + var sourceNode, fftBins, analyser; + + analyser = audioContext.createAnalyser(); + analyser.fftSize = 512; + analyser.smoothingTimeConstant = smoothing; + fftBins = new Float32Array(analyser.frequencyBinCount); + + if (stream.jquery) stream = stream[0]; + if (stream instanceof HTMLAudioElement || stream instanceof HTMLVideoElement) { + //Audio Tag + sourceNode = audioContext.createMediaElementSource(stream); + if (typeof play === 'undefined') play = true; + threshold = threshold || -50; + } else { + //WebRTC Stream + sourceNode = audioContext.createMediaStreamSource(stream); + threshold = threshold || -50; + } + + sourceNode.connect(analyser); + if (play) analyser.connect(audioContext.destination); + + harker.speaking = false; + + harker.suspend = function() { + return audioContext.suspend(); + } + harker.resume = function() { + return audioContext.resume(); + } + Object.defineProperty(harker, 'state', { get: function() { + return audioContext.state; + }}); + audioContext.onstatechange = function() { + harker.emit('state_change', audioContext.state); + } + + harker.setThreshold = function(t) { + threshold = t; + }; + + harker.setInterval = function(i) { + interval = i; + }; + + harker.stop = function() { + running = false; + harker.emit('volume_change', -100, threshold); + if (harker.speaking) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + analyser.disconnect(); + sourceNode.disconnect(); + }; + harker.speakingHistory = []; + for (var i = 0; i < history; i++) { + harker.speakingHistory.push(0); + } + + // Poll the analyser node to determine if speaking + // and emit events if changed + var looper = function() { + setTimeout(function() { + + //check if stop has been called + if(!running) { + return; + } + + var currentVolume = getMaxVolume(analyser, fftBins); + + harker.emit('volume_change', currentVolume, threshold); + + var history = 0; + if (currentVolume > threshold && !harker.speaking) { + // trigger quickly, short history + for (var i = harker.speakingHistory.length - 3; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history >= 2) { + harker.speaking = true; + harker.emit('speaking'); + } + } else if (currentVolume < threshold && harker.speaking) { + for (var i = 0; i < harker.speakingHistory.length; i++) { + history += harker.speakingHistory[i]; + } + if (history == 0) { + harker.speaking = false; + harker.emit('stopped_speaking'); + } + } + harker.speakingHistory.shift(); + harker.speakingHistory.push(0 + (currentVolume > threshold)); + + looper(); + }, interval); + }; + looper(); + + return harker; + } + + },{"wildemitter":2}],2:[function(require,module,exports){ + /* + WildEmitter.js is a slim little event emitter by @henrikjoreteg largely based + on @visionmedia's Emitter from UI Kit. + + Why? I wanted it standalone. + + I also wanted support for wildcard emitters like this: + + emitter.on('*', function (eventName, other, event, payloads) { + + }); + + emitter.on('somenamespace*', function (eventName, payloads) { + + }); + + Please note that callbacks triggered by wildcard registered events also get + the event name as the first argument. + */ + + module.exports = WildEmitter; + + function WildEmitter() { } + + WildEmitter.mixin = function (constructor) { + var prototype = constructor.prototype || constructor; + + prototype.isWildEmitter= true; + + // Listen on the given `event` with `fn`. Store a group name if present. + prototype.on = function (event, groupName, fn) { + this.callbacks = this.callbacks || {}; + var hasGroup = (arguments.length === 3), + group = hasGroup ? arguments[1] : undefined, + func = hasGroup ? arguments[2] : arguments[1]; + func._groupName = group; + (this.callbacks[event] = this.callbacks[event] || []).push(func); + return this; + }; + + // Adds an `event` listener that will be invoked a single + // time then automatically removed. + prototype.once = function (event, groupName, fn) { + var self = this, + hasGroup = (arguments.length === 3), + group = hasGroup ? arguments[1] : undefined, + func = hasGroup ? arguments[2] : arguments[1]; + function on() { + self.off(event, on); + func.apply(this, arguments); + } + this.on(event, group, on); + return this; + }; + + // Unbinds an entire group + prototype.releaseGroup = function (groupName) { + this.callbacks = this.callbacks || {}; + var item, i, len, handlers; + for (item in this.callbacks) { + handlers = this.callbacks[item]; + for (i = 0, len = handlers.length; i < len; i++) { + if (handlers[i]._groupName === groupName) { + //console.log('removing'); + // remove it and shorten the array we're looping through + handlers.splice(i, 1); + i--; + len--; + } + } + } + return this; + }; + + // Remove the given callback for `event` or all + // registered callbacks. + prototype.off = function (event, fn) { + this.callbacks = this.callbacks || {}; + var callbacks = this.callbacks[event], + i; + + if (!callbacks) return this; + + // remove all handlers + if (arguments.length === 1) { + delete this.callbacks[event]; + return this; + } + + // remove specific handler + i = callbacks.indexOf(fn); + callbacks.splice(i, 1); + if (callbacks.length === 0) { + delete this.callbacks[event]; + } + return this; + }; + + /// Emit `event` with the given args. + // also calls any `*` handlers + prototype.emit = function (event) { + this.callbacks = this.callbacks || {}; + var args = [].slice.call(arguments, 1), + callbacks = this.callbacks[event], + specialCallbacks = this.getWildcardCallbacks(event), + i, + len, + item, + listeners; + + if (callbacks) { + listeners = callbacks.slice(); + for (i = 0, len = listeners.length; i < len; ++i) { + if (!listeners[i]) { + break; + } + listeners[i].apply(this, args); + } + } + + if (specialCallbacks) { + len = specialCallbacks.length; + listeners = specialCallbacks.slice(); + for (i = 0, len = listeners.length; i < len; ++i) { + if (!listeners[i]) { + break; + } + listeners[i].apply(this, [event].concat(args)); + } + } + + return this; + }; + + // Helper for for finding special wildcard event handlers that match the event + prototype.getWildcardCallbacks = function (eventName) { + this.callbacks = this.callbacks || {}; + var item, + split, + result = []; + + for (item in this.callbacks) { + split = item.split('*'); + if (item === '*' || (split.length === 2 && eventName.slice(0, split[0].length) === split[0])) { + result = result.concat(this.callbacks[item]); + } + } + return result; + }; + + }; + + WildEmitter.mixin(WildEmitter); + + },{}]},{},[1])(1) + }); + ; \ No newline at end of file diff --git a/Veil/assets/sound/recorded.mp3 b/Veil/assets/sound/recorded.mp3 new file mode 100644 index 000000000..359bf15b0 Binary files /dev/null and b/Veil/assets/sound/recorded.mp3 differ diff --git a/Veil/components/Base/Animation.vue b/Veil/components/Base/Animation.vue index 4e5bb68bc..f3367cf33 100644 --- a/Veil/components/Base/Animation.vue +++ b/Veil/components/Base/Animation.vue @@ -10,6 +10,9 @@ import NotificationAnimation from '@Veil/assets/animations/notification.json' import PowerAnimation from '@Veil/assets/animations/power.json' import SettingsAnimation from '@Veil/assets/animations/settings.json' import WalletAnimation from '@Veil/assets/animations/wallet.json' +import WaveAnimation from '@Veil/assets/animations/Wave.json' +import RecordAnimation from '@Veil/assets/animations/Record.json' +import WritingAnimation from '@Veil/assets/animations/Writing.json' import { computed } from "@vue/reactivity" const animations = { @@ -24,6 +27,9 @@ const animations = { "power": PowerAnimation, "settings": SettingsAnimation, "wallet": WalletAnimation, + "wave": WaveAnimation, + "record": RecordAnimation, + "writing": WritingAnimation } const props = defineProps<{ @@ -41,7 +47,7 @@ const animation = computed(() => { - \ No newline at end of file diff --git a/Veil/components/Base/Choose.vue b/Veil/components/Base/Choose.vue new file mode 100644 index 000000000..5715671a5 --- /dev/null +++ b/Veil/components/Base/Choose.vue @@ -0,0 +1,165 @@ + + + + + \ No newline at end of file diff --git a/Veil/components/Base/ControlBar.vue b/Veil/components/Base/ControlBar.vue index 56cc2d56f..a20b67498 100644 --- a/Veil/components/Base/ControlBar.vue +++ b/Veil/components/Base/ControlBar.vue @@ -1,49 +1,64 @@ - \ No newline at end of file + +.bg-right { + background-color: var(--main-bg); + height: 16px; + position: absolute; + right: 0; + width: calc(100% - 170px); +} + +.collapsed-sidebar.bg-right { + width: 100%; +} + +.bg-left { + background-color: var(--sidebar-bg); + height: 16px; + position: absolute; + left: 0; + width: 170px; +} + +.collapsed-sidebar.bg-left { + width: 0; +} + +.tesla-warning { + display: none; +} +@Veil/state/common \ No newline at end of file diff --git a/Veil/components/Base/Icon.vue b/Veil/components/Base/Icon.vue index 42a6bba0a..01e0af024 100644 --- a/Veil/components/Base/Icon.vue +++ b/Veil/components/Base/Icon.vue @@ -1,15 +1,16 @@ - +@Veil/state/common \ No newline at end of file diff --git a/Veil/components/Base/Menu.vue b/Veil/components/Base/Menu.vue new file mode 100644 index 000000000..e9baff4bb --- /dev/null +++ b/Veil/components/Base/Menu.vue @@ -0,0 +1,159 @@ + + + + + \ No newline at end of file diff --git a/Veil/components/Base/MenuItem.vue b/Veil/components/Base/MenuItem.vue new file mode 100644 index 000000000..f24493d87 --- /dev/null +++ b/Veil/components/Base/MenuItem.vue @@ -0,0 +1,25 @@ + + + + + \ No newline at end of file diff --git a/Veil/components/Base/SubMenu.vue b/Veil/components/Base/SubMenu.vue new file mode 100644 index 000000000..90eb2c297 --- /dev/null +++ b/Veil/components/Base/SubMenu.vue @@ -0,0 +1,78 @@ + + + + + \ No newline at end of file diff --git a/Veil/components/Calendar/CalSidebar.vue b/Veil/components/Calendar/CalSidebar.vue new file mode 100644 index 000000000..6a874ee79 --- /dev/null +++ b/Veil/components/Calendar/CalSidebar.vue @@ -0,0 +1,344 @@ + + + + + +@Veil/state/common \ No newline at end of file diff --git a/Veil/components/Calendar/FullCalendar.vue b/Veil/components/Calendar/FullCalendar.vue new file mode 100644 index 000000000..6e969192c --- /dev/null +++ b/Veil/components/Calendar/FullCalendar.vue @@ -0,0 +1,97 @@ + + + + \ No newline at end of file diff --git a/Veil/components/Composer/ComposerBody.vue b/Veil/components/Composer/ComposerBody.vue index 838346b33..a563c2320 100644 --- a/Veil/components/Composer/ComposerBody.vue +++ b/Veil/components/Composer/ComposerBody.vue @@ -1,31 +1,51 @@