├── .npmrc ├── versions.json ├── .eslintignore ├── type.d.ts ├── publish-icon.png ├── mixa_thumbnail.png ├── mixa_obs_thumbnail.png ├── types.ts ├── styles.css ├── .editorconfig ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── patches └── s3-sync-client+3.0.3.patch ├── package.json ├── LICENSE ├── esbuild.config.mjs ├── README.md ├── .github └── workflows │ └── releases.yml └── src ├── sync.ts └── main.ts /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.2": "0.15.0" 3 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /type.d.ts: -------------------------------------------------------------------------------- 1 | declare module 's3-sync-client'; 2 | 3 | declare module 'mime-types'; -------------------------------------------------------------------------------- /publish-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixasite/obsidian-mixa/HEAD/publish-icon.png -------------------------------------------------------------------------------- /mixa_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixasite/obsidian-mixa/HEAD/mixa_thumbnail.png -------------------------------------------------------------------------------- /mixa_obs_thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mixasite/obsidian-mixa/HEAD/mixa_obs_thumbnail.png -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | export interface MixaSettings { 2 | secretToken: string 3 | siteFolder: string 4 | publishExternal: boolean 5 | subdomain: string 6 | siteUrl: string 7 | siteEditUrl: string 8 | } -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This CSS file will be included with your plugin, and 4 | available in the app when your plugin is enabled. 5 | 6 | If your plugin does not need CSS, delete this file. 7 | 8 | */ 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "mixa", 3 | "name": "Mixa", 4 | "version": "1.0.2", 5 | "minAppVersion": "0.15.0", 6 | "description": "Publish your notes and blog posts with Mixa directly from Obsidian", 7 | "author": "Mixa", 8 | "authorUrl": "https://mixa.site", 9 | "isDesktopOnly": true 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "inlineSourceMap": true, 5 | "inlineSources": true, 6 | "module": "ESNext", 7 | "target": "ES6", 8 | "allowJs": true, 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "importHelpers": true, 12 | "isolatedModules": true, 13 | "strictNullChecks": true, 14 | "lib": [ 15 | "DOM", 16 | "ES5", 17 | "ES6", 18 | "ES7" 19 | ] 20 | }, 21 | "include": [ 22 | "**/*.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /patches/s3-sync-client+3.0.3.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/s3-sync-client/lib/commands/bucket-with-local.js b/node_modules/s3-sync-client/lib/commands/bucket-with-local.js 2 | index bc5a23c..8862c09 100644 3 | --- a/node_modules/s3-sync-client/lib/commands/bucket-with-local.js 4 | +++ b/node_modules/s3-sync-client/lib/commands/bucket-with-local.js 5 | @@ -24,7 +24,7 @@ async function bucketWithLocal(localDir, bucketPrefix, options = {}) { 6 | this.listBucketObjects(bucket, { prefix }), 7 | ]); 8 | if (prefix !== '') { 9 | - relocations.push(['', prefix]); 10 | + relocations.push(['', prefix.replace(/\/$/, '')]); 11 | } 12 | sourceObjects.forEach((sourceObject) => sourceObject.applyFilters(filters)); 13 | const includedSourceObjects = sourceObjects.filter((sourceObject) => sourceObject.isIncluded()); 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-mixa", 3 | "version": "1.0.1", 4 | "description": "Publish your notes and blog posts with Mixa directly from Obsidian", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json", 10 | "postinstall": "patch-package" 11 | }, 12 | "keywords": [], 13 | "author": "", 14 | "license": "MIT", 15 | "devDependencies": { 16 | "@types/node": "^16.11.6", 17 | "@typescript-eslint/eslint-plugin": "5.29.0", 18 | "@typescript-eslint/parser": "5.29.0", 19 | "builtin-modules": "3.3.0", 20 | "esbuild": "0.17.3", 21 | "obsidian": "latest", 22 | "tslib": "2.4.0", 23 | "typescript": "4.7.4" 24 | }, 25 | "dependencies": { 26 | "@aws-sdk/abort-controller": "^3.374.0", 27 | "@aws-sdk/client-s3": "^3.266.0", 28 | "axios": "^1.3.2", 29 | "mime-types": "^2.1.35", 30 | "patch-package": "^8.0.0", 31 | "s3-sync-client": "^3.0.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = 6 | `/* 7 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 8 | if you want to view the source, please visit the github repository of this plugin 9 | */ 10 | `; 11 | 12 | const prod = (process.argv[2] === "production"); 13 | 14 | const context = await esbuild.context({ 15 | banner: { 16 | js: banner, 17 | }, 18 | entryPoints: ["src/main.ts"], 19 | bundle: true, 20 | external: [ 21 | "obsidian", 22 | "electron", 23 | "@codemirror/autocomplete", 24 | "@codemirror/collab", 25 | "@codemirror/commands", 26 | "@codemirror/language", 27 | "@codemirror/lint", 28 | "@codemirror/search", 29 | "@codemirror/state", 30 | "@codemirror/view", 31 | "@lezer/common", 32 | "@lezer/highlight", 33 | "@lezer/lr", 34 | ...builtins], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mixa 2 | Mixa is a 1-click no-code site builder with tons of features for you to share your notes and blog posts in seconds. 3 | 4 | - [Mixa](https://mixa.site) 5 | - [Docs](https://mixa.site/docs) 6 | - [Roadmap](https://trello.com/b/UG0i4eJR/mixa-roadmap) 7 | - [Discord](https://discord.com/invite/z3xNMHjUt7) 8 | - [Community](https://mixasite.talkyard.net/) 9 | - [Email support@mixa.site](mailto:support@mixa.site) 10 | 11 | ## Release notes 12 | ### 1.0.1 13 | - Now you can see what is going to be published by clicking "Show Changes" Button in the plugin settings page. It will show which files will be published and which ones are deleted. If all looks good, you can click "Publish" to publish your changes with peace of mind 14 | - You can disable publishing individual files by adding `draft: true` frontmatter 15 | ``` 16 | --- 17 | draft: true 18 | --- 19 | 20 | Some note that will not be published till draft frontmatter is removed or changed to false 21 | ``` 22 | 23 | 24 | ## Mixa Demo 25 | ### With Obsidian 26 | [![Mixa Obsidian Demo](./mixa_obs_thumbnail.png)](https://youtu.be/FrvZd2pdX-g) 27 | 28 | ### With .zip file 29 | [![Mixa Demo](./mixa_thumbnail.png)](https://youtu.be/-Ylp5Dm9sVo) 30 | 31 | ## How to use 32 | 1. Login to the [mixa.site](https://mixa.site) 33 | 2. Create a new site from the dashboard 34 | 3. Generate a secret token from the site settings in the dashboard 35 | 4. Paste the token in Obsidian Mixa plugin and Publish your site 36 | 5. Once you setup your Obsidian-Mixa integration, you can also use the publish button and publish your changes in 1-click 37 | ![](./publish-icon.png) 38 | 39 | ## You focus on your content, we take care of the rest 40 | Create a blazing-fast, beautiful website with great SEO under your own domain. Bring your notes or blog posts and publish in seconds. Enable the features you need using the live-preview editor. No coding is needed. Use your favorite tool to write like Obisidian. 41 | 42 | ### 1-Click Publish 43 | - Zero-code, no-hassle publishing 44 | - Notes and Blogs are first-class citizens 45 | - Create your pages in md, mdx, or html 46 | - Keep your folder/file structure however you want 47 | 48 | ### Looks good everyway 49 | - 30 themes to choose from 50 | - Mobile-first design 51 | - Beautiful subdomain or bring your custom domain 52 | 53 | ### Blazing fast 54 | - Your site is stored all over the world for faster load time 55 | - Images are optimized for faster load too 56 | - Your site is optimized for SEO 57 | 58 | ### All-In-One Solution 59 | - Fully customizable site with live preview. 60 | - Sidebar navigation 61 | - Header navigation 62 | - Site search 63 | - Table of contents 64 | - Google Analytics 65 | - Light/Dark mode switcher 66 | - Inject your custom JavaScript and CSS 67 | -------------------------------------------------------------------------------- /.github/workflows/releases.yml: -------------------------------------------------------------------------------- 1 | name: Build obsidian plugin 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "*" # Push events to matching any tag format, i.e. 1.0, 20.15.10 8 | 9 | env: 10 | PLUGIN_NAME: obsidian-mixa 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: "14.x" # You might need to adjust this value to your own version 22 | - name: Build 23 | id: build 24 | run: | 25 | yarn 26 | yarn run build --if-present 27 | mkdir ${{ env.PLUGIN_NAME }} 28 | cp main.js manifest.json ${{ env.PLUGIN_NAME }} 29 | zip -r ${{ env.PLUGIN_NAME }}.zip ${{ env.PLUGIN_NAME }} 30 | ls 31 | echo "::set-output name=tag_name::$(git tag --sort version:refname | tail -n 1)" 32 | - name: Create Release 33 | id: create_release 34 | uses: actions/create-release@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | VERSION: ${{ github.ref }} 38 | with: 39 | tag_name: ${{ github.ref }} 40 | release_name: ${{ github.ref }} 41 | draft: false 42 | prerelease: false 43 | - name: Upload zip file 44 | id: upload-zip 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} 50 | asset_path: ./${{ env.PLUGIN_NAME }}.zip 51 | asset_name: ${{ env.PLUGIN_NAME }}-${{ steps.build.outputs.tag_name }}.zip 52 | asset_content_type: application/zip 53 | - name: Upload main.js 54 | id: upload-main 55 | uses: actions/upload-release-asset@v1 56 | env: 57 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 58 | with: 59 | upload_url: ${{ steps.create_release.outputs.upload_url }} 60 | asset_path: ./main.js 61 | asset_name: main.js 62 | asset_content_type: text/javascript 63 | - name: Upload manifest.json 64 | id: upload-manifest 65 | uses: actions/upload-release-asset@v1 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | with: 69 | upload_url: ${{ steps.create_release.outputs.upload_url }} 70 | asset_path: ./manifest.json 71 | asset_name: manifest.json 72 | asset_content_type: application/json 73 | # - name: Upload styles.css 74 | # id: upload-css 75 | # uses: actions/upload-release-asset@v1 76 | # env: 77 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 78 | # with: 79 | # upload_url: ${{ steps.create_release.outputs.upload_url }} 80 | # asset_path: ./styles.css 81 | # asset_name: styles.css 82 | # asset_content_type: text/css 83 | -------------------------------------------------------------------------------- /src/sync.ts: -------------------------------------------------------------------------------- 1 | import { FileSystemAdapter, MetadataCache, Notice, Vault } from "obsidian"; 2 | import axios from 'axios'; 3 | import { MixaSettings } from "types"; 4 | import { join } from 'path' 5 | import { existsSync } from 'fs' 6 | import { S3Client } from "@aws-sdk/client-s3"; 7 | import S3SyncClient from 's3-sync-client'; 8 | import mime from 'mime-types' 9 | 10 | const API_BASE = 'https://app.mixa.site/api'; 11 | const BUCKET_NAME = 'resource.mixa.site'; 12 | const S3_URL = 'https://s3.us-east-1.amazonaws.com'; 13 | const S3_REGION = 'us-east-1'; 14 | 15 | const client = axios.create({ 16 | baseURL: API_BASE, 17 | }); 18 | 19 | function stripSlash(str: string) { 20 | return str?.replace(/^\/*|\/*$/g, ""); 21 | } 22 | 23 | function getBasePath() { 24 | const adapter = app.vault.adapter; 25 | if (adapter instanceof FileSystemAdapter) { 26 | return adapter.getBasePath(); 27 | } 28 | new Notice('Your system does not support files access'); 29 | throw Error('Your system does not support files access'); 30 | } 31 | 32 | export async function getSiteData(secretToken: string) { 33 | if (!secretToken) return; 34 | const { data } = await client.get(`/site/token/${secretToken}/info`); 35 | return data; 36 | } 37 | 38 | export async function getAdditionalFiles(vault: Vault, siteFolder: string) { 39 | const sanitizeRegex = /\.*\/*(.*?)?( *\|.*)?$/g; 40 | const referenceRegex = /!?\[(?:\[|\]\()(.*?)(?:\]\]|\)|\|)/g; 41 | const matches = (await Promise.all(vault.getMarkdownFiles() 42 | .filter(f => f.path.startsWith(siteFolder)) 43 | .map(f => vault.cachedRead(f)))) 44 | .join() 45 | .matchAll(referenceRegex); 46 | const referencedFiles: string[] = []; 47 | for (const match of matches) { 48 | referencedFiles.push(match[1].replace(sanitizeRegex, '$1')); 49 | } 50 | 51 | // find actual paths of the referenced files 52 | // check if they're not part of the siteFolder 53 | const localFiles = vault.getFiles().map(f => f.path).map(f => f.replace(sanitizeRegex, '$1')).filter(f => !f.startsWith(siteFolder)); 54 | // add them to sync 55 | const filesToCopy = localFiles.filter(lf => referencedFiles.some(rf => lf.endsWith(rf) || lf.endsWith(`${rf}.md`))); 56 | return filesToCopy; 57 | } 58 | 59 | function getDraftFiles(vault: Vault, metadataCache: MetadataCache): string[] { 60 | return vault.getMarkdownFiles() 61 | .map(f => f.path) 62 | .filter(path => { 63 | const cache = metadataCache.getCache(path) 64 | const draftVal = cache?.frontmatter?.draft 65 | return draftVal === true || draftVal === 'true' 66 | }) 67 | } 68 | 69 | export async function syncData(settings: MixaSettings, vault: Vault, metadataCache: MetadataCache, isDryRun?: boolean) { 70 | // should give a notice to the user here for mobile devices (without node envrionment) and exit 71 | const rootFolderAbsPath = getBasePath(); 72 | const siteFolder = stripSlash(settings.siteFolder); 73 | const localFolder = join(rootFolderAbsPath, siteFolder); 74 | 75 | if (!existsSync(localFolder)) { 76 | throw Error(`Site folder you specified does not exist: ${settings.siteFolder}`); 77 | } 78 | 79 | let additionalFiles: string[] = []; 80 | if (settings.publishExternal) { 81 | try { 82 | additionalFiles = await getAdditionalFiles(vault, siteFolder); 83 | } catch (err) { 84 | console.error(err); 85 | } 86 | } 87 | const draftFiles = getDraftFiles(vault, metadataCache) 88 | 89 | const { data: creds } = await client.get(`/site/token/${settings.secretToken}/auth`); 90 | if (!creds || !Object.keys(creds).length) { 91 | throw Error(`Could not upload the files due to an unknown error, please contact support@mixa.site`); 92 | } 93 | 94 | const s3Path = `s3://${BUCKET_NAME}/${settings.subdomain}/`; 95 | try { 96 | const res = await syncToS3(creds, rootFolderAbsPath, siteFolder, s3Path, additionalFiles, draftFiles, isDryRun); 97 | if (isDryRun) { 98 | return res 99 | } 100 | await client.post(`/site/token/${settings.secretToken}/build`); 101 | return res 102 | } catch (err) { 103 | console.error(err); 104 | throw err; 105 | } 106 | } 107 | 108 | async function syncToS3(creds: any, rootFolderAbsPath: string, siteFolder: string, s3Path: string, additionalFiles: string[], draftFiles: string[], isDryRun?: boolean) { 109 | const s3Client = new S3Client({ 110 | region: S3_REGION, 111 | forcePathStyle: true, 112 | credentials: creds, 113 | endpoint: S3_URL, 114 | }); 115 | const { sync } = new S3SyncClient({ client: s3Client }); 116 | 117 | console.log(`syncing files to s3. local: ${siteFolder}, remote: ${s3Path}`); 118 | console.log(`additional files to send: ${additionalFiles}`); 119 | console.log(`draft files, not send: ${draftFiles}`); 120 | 121 | const res = await sync(rootFolderAbsPath, s3Path, { 122 | del: true, 123 | dryRun: !!isDryRun, 124 | relocations: [ 125 | [siteFolder, ''] 126 | ], 127 | filters: [ 128 | { exclude: (key: string) => key.split('/').some(n => n.startsWith('.')) || (siteFolder && !key.startsWith(`${siteFolder}/`) && !additionalFiles.contains(key)) }, 129 | { exclude: (key: string) => draftFiles.contains(key) }, 130 | ], 131 | commandInput: { 132 | ContentType: (syncCommandInput: {Key: string}) => ( 133 | mime.lookup(syncCommandInput.Key) 134 | ), 135 | }, 136 | }); 137 | return { 138 | deletions: res.deletions || [], 139 | uploads: res.uploads || [], 140 | ignored: draftFiles.map(p => ({id: p})) || [], 141 | } 142 | } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { App, debounce, Notice, Plugin, PluginSettingTab, Setting } from 'obsidian'; 2 | import { getSiteData, syncData } from './sync'; 3 | import { MixaSettings } from 'types'; 4 | 5 | const DEFAULT_SETTINGS: MixaSettings = { 6 | secretToken: '', 7 | siteFolder: '', 8 | subdomain: '', 9 | publishExternal: false, 10 | siteUrl: '', 11 | siteEditUrl: '', 12 | }; 13 | 14 | export default class MixaPlugin extends Plugin { 15 | settings: MixaSettings; 16 | 17 | async onload() { 18 | await this.loadSettings(); 19 | 20 | // This creates an icon in the left ribbon. 21 | this.addRibbonIcon('paper-plane', this.settings.siteUrl ? `Publish to ${this.settings.siteUrl.replace(/(^\w+:|^)\/\//, '')} with Mixa` : 'Publish with Mixa', async (evt: MouseEvent) => { 22 | if (!this.settings.subdomain) { 23 | new Notice('Please add a valid Secret Token. You can find it in your Mixa Dashboard'); 24 | return; 25 | } 26 | 27 | new Notice('Publishing your site, hang tight...'); 28 | try { 29 | await syncData(this.settings, this.app.vault, this.app.metadataCache); 30 | new Notice('Changes are published to your site successfully'); 31 | } catch (error) { 32 | new Notice(error.message || 'Failed to publish your site. Please try again, or contact support@mixa.site'); 33 | } 34 | }); 35 | 36 | // This adds a settings tab so the user can configure various aspects of the plugin 37 | this.addSettingTab(new MixaSettingTab(this.app, this)); 38 | } 39 | 40 | onunload() { 41 | 42 | } 43 | 44 | async loadSettings() { 45 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 46 | } 47 | 48 | async saveSettings() { 49 | await this.saveData(this.settings); 50 | } 51 | } 52 | 53 | class MixaSettingTab extends PluginSettingTab { 54 | plugin: MixaPlugin; 55 | infoDiv: HTMLDivElement; 56 | fileDiv: HTMLDivElement; 57 | 58 | constructor(app: App, plugin: MixaPlugin) { 59 | super(app, plugin); 60 | this.plugin = plugin; 61 | } 62 | 63 | async updateSiteSettings(site: any | null, msg?: string): Promise { 64 | this.plugin.settings.subdomain = site?.subdomain || ''; 65 | this.plugin.settings.siteUrl = site?.siteUrl || ''; 66 | this.plugin.settings.siteEditUrl = site?.siteEditUrl || ''; 67 | this.infoDiv.textContent = msg ? msg : ''; 68 | this.fileDiv.innerHTML = '' 69 | await this.plugin.saveSettings(); 70 | } 71 | 72 | renderFileInfo(files: { deletions: { id: string; }[]; uploads: { id: string; }[]; ignored: { id: string; }[] }) { 73 | if (!files.uploads.length && !files.deletions.length) { 74 | this.fileDiv.innerHTML = "Nothing to upload" 75 | } else { 76 | this.fileDiv.innerHTML = '
' + 77 | files.uploads.map((file: { id: string }) => `
${file.id}
`).join('') + 78 | files.deletions.map((file: { id: string }) => `
${file.id} - deleted
`).join('') + 79 | files.ignored.map((file: { id: string }) => `
${file.id} - ignored
`).join('') + 80 | '
'; 81 | } 82 | } 83 | 84 | display(): void { 85 | const { containerEl } = this; 86 | 87 | containerEl.empty(); 88 | 89 | containerEl.createEl('h2', { text: 'Mixa Settings' }); 90 | containerEl.createEl("a", undefined, link => { 91 | link.href = 'https://mixa.site'; 92 | link.innerText = `https://mixa.site`; 93 | }); 94 | 95 | new Setting(containerEl) 96 | .setName('Secret Token') 97 | .setDesc('Generate it from your Mixa Dashboard for the site you want to publish') 98 | .addText(text => text 99 | .setPlaceholder('Enter your secret token') 100 | .setValue(this.plugin.settings.secretToken) 101 | .onChange(debounce(async (value) => { 102 | this.plugin.settings.secretToken = value; 103 | await this.plugin.saveSettings(); 104 | 105 | try { 106 | this.infoDiv.textContent = 'Parsing your secret token'; 107 | const site = await getSiteData(value); 108 | this.infoDiv.textContent = ''; 109 | if (site) { 110 | await this.updateSiteSettings(site); 111 | } else { 112 | await this.updateSiteSettings(null, 'Please add a valid Secret Token. You can find it in your Mixa Dashboard'); 113 | } 114 | } catch (error) { 115 | await this.updateSiteSettings(null, 'Please add a valid Secret Token. You can find it in your Mixa Dashboard'); 116 | error.response.data.errorText && new Notice(error.response.data.errorText); 117 | } 118 | refreshSiteInfo(containerEl, this.plugin.settings); 119 | 120 | }, 1000, true))); 121 | 122 | new Setting(containerEl) 123 | .setName('Site Folder') 124 | .setDesc('Type the folder you want to publish. Leave it empty to publish all your documents.') 125 | .addText(text => text 126 | .setPlaceholder('e.g. /notes') 127 | .setValue(this.plugin.settings.siteFolder) 128 | .onChange(async (value) => { 129 | this.plugin.settings.siteFolder = value; 130 | await this.plugin.saveSettings(); 131 | })); 132 | 133 | new Setting(containerEl) 134 | .setName('Publish Referenced Files') 135 | .setDesc('If you specify a Site Folder and your notes are referencing notes and images outside of this folder, do you want to publish referenced notes and images too? (This has no effect when there is no "Site Folder" specified above)') 136 | .addToggle(text => text 137 | .setValue(this.plugin.settings.publishExternal) 138 | .onChange(async (value) => { 139 | this.plugin.settings.publishExternal = value; 140 | await this.plugin.saveSettings(); 141 | })); 142 | 143 | createSiteInfo(containerEl, this.plugin.settings); 144 | refreshSiteInfo(containerEl, this.plugin.settings); 145 | 146 | new Setting(containerEl) 147 | .addButton((button) => { 148 | button.setButtonText("Show Changes").onClick(async (e) => { 149 | if (!this.plugin.settings.subdomain) { 150 | this.infoDiv.textContent = 'Please add a valid Secret Token. You can find it in your Mixa Dashboard' 151 | return; 152 | } 153 | try { 154 | this.fileDiv.innerHTML = ''; 155 | this.infoDiv.textContent = 'We are checking the diff...'; 156 | const files = await syncData(this.plugin.settings, this.app.vault, this.app.metadataCache, true); 157 | this.renderFileInfo(files) 158 | this.infoDiv.textContent = ''; 159 | } catch (error) { 160 | this.infoDiv.textContent = error.message || 'Failed to get the diff for your site. Please try again, or contact support@mixa.site'; 161 | this.fileDiv.innerHTML = ''; 162 | } 163 | }); 164 | }) 165 | .addButton((button) => { 166 | button.setButtonText("Publish").onClick(async (e) => { 167 | if (!this.plugin.settings.subdomain) { 168 | this.infoDiv.textContent = 'Please add a valid Secret Token. You can find it in your Mixa Dashboard' 169 | return; 170 | } 171 | try { 172 | this.fileDiv.innerHTML = ''; 173 | this.infoDiv.textContent = 'We are publishing your site, hang tight'; 174 | const files = await syncData(this.plugin.settings, this.app.vault, this.app.metadataCache); 175 | this.infoDiv.textContent = 'Changes are published to your site successfully'; 176 | this.renderFileInfo(files) 177 | } catch (error) { 178 | this.infoDiv.textContent = error.message || 'Failed to publish your site. Please try again, or contact support@mixa.site'; 179 | } 180 | }); 181 | }); 182 | 183 | this.infoDiv = containerEl.createDiv(); 184 | this.fileDiv = containerEl.createDiv(); 185 | } 186 | } 187 | 188 | function createSiteInfo(containerEl: HTMLElement, settings: MixaSettings) { 189 | createSettingItemLink(containerEl, 'Site', 'Check out your live site here', 'mixa-site-url'); 190 | createSettingItemLink(containerEl, 'Site Edit', 'You can edit your site with Mixa\'s live preview editor', 'mixa-site-edit-url'); 191 | } 192 | 193 | function refreshSiteInfo(containerEl: HTMLElement, settings: MixaSettings) { 194 | if (settings.siteUrl) { 195 | (containerEl.find('#mixa-site-url') as HTMLAnchorElement).href = settings.siteUrl; 196 | containerEl.find('#mixa-site-url').innerText = settings.siteUrl; 197 | (containerEl.find('#mixa-site-edit-url') as HTMLAnchorElement).href = settings.siteEditUrl; 198 | containerEl.find('#mixa-site-edit-url').innerText = settings.siteEditUrl; 199 | containerEl.findAll('.mixa-setting-item').forEach(e => e.show()); 200 | } else { 201 | containerEl.findAll('.mixa-setting-item').forEach(e => e.hide()); 202 | } 203 | } 204 | 205 | function createSettingItemLink(containerEl: HTMLElement, name: string, description: string, linkId: string) { 206 | const settingItem = containerEl.createDiv({ cls: 'setting-item mixa-setting-item' }); 207 | const info = settingItem.createDiv({ cls: 'setting-item-info' }); 208 | const control = settingItem.createDiv({ cls: 'setting-item-control' }); 209 | 210 | info.createDiv({ cls: 'setting-item-name', text: name }); 211 | info.createDiv({ cls: 'setting-item-description', text: description }); 212 | control.createEl("a", undefined, linkEl => { 213 | linkEl.id = linkId; 214 | }); 215 | } 216 | --------------------------------------------------------------------------------