├── .eslintrc.json
├── .github
├── PULL_REQUEST_TEMPLATE
│ ├── plugin.md
│ └── theme.md
├── pull_request_template.md
└── workflows
│ ├── plugin-stat.yml
│ ├── skip.yml
│ ├── stale.yml
│ ├── validate-plugin-entry.yml
│ └── validate-theme-entry.yml
├── .gitignore
├── README.md
├── cla.md
├── community-css-themes-removed.json
├── community-css-themes.json
├── community-plugin-deprecation.json
├── community-plugin-stats.json
├── community-plugins-removed.json
├── community-plugins.json
├── community-snippets.json
├── dark.png
├── desktop-releases.json
├── light.png
├── package.json
└── plugin-review.md
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["plugin:json/recommended"]
3 | }
4 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/plugin.md:
--------------------------------------------------------------------------------
1 | # I am submitting a new Community Plugin
2 |
3 | ## Repo URL
4 |
5 |
6 | Link to my plugin:
7 |
8 | ## Release Checklist
9 | - [ ] I have tested the plugin on
10 | - [ ] Windows
11 | - [ ] macOS
12 | - [ ] Linux
13 | - [ ] Android _(if applicable)_
14 | - [ ] iOS _(if applicable)_
15 | - [ ] My GitHub release contains all required files (as individual files, not just in the source.zip / source.tar.gz)
16 | - [ ] `main.js`
17 | - [ ] `manifest.json`
18 | - [ ] `styles.css` _(optional)_
19 | - [ ] GitHub release name matches the exact version number specified in my manifest.json (_**Note:** Use the exact version number, don't include a prefix `v`_)
20 | - [ ] The `id` in my `manifest.json` matches the `id` in the `community-plugins.json` file.
21 | - [ ] My README.md describes the plugin's purpose and provides clear usage instructions.
22 | - [ ] I have read the developer policies at https://docs.obsidian.md/Developer+policies, and have assessed my plugins's adherence to these policies.
23 | - [ ] I have read the tips in https://docs.obsidian.md/Plugins/Releasing/Plugin+guidelines and have self-reviewed my plugin to avoid these common pitfalls.
24 | - [ ] I have added a license in the LICENSE file.
25 | - [ ] My project respects and is compatible with the original license of any code from other plugins that I'm using.
26 | I have given proper attribution to these other projects in my `README.md`.
27 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE/theme.md:
--------------------------------------------------------------------------------
1 | # I am submitting a new Community Theme
2 |
3 | ## Repo URL
4 |
5 |
6 | Link to my theme:
7 |
8 |
9 | ## Theme checklist
10 |
11 |
12 | - [ ] My repo contains all required files (please *do not* add them to this `obsidian-releases` repo).
13 | - [ ] `manifest.json`
14 | - [ ] `theme.css`
15 | - [ ] The screenshot file (16:9 aspect ratio, recommended size is 512px by 288px for fast loading).
16 | - [ ] I have indicated which modes (dark, light, or both) are compatible with my theme.
17 | - [ ] I have read the developer policies at https://docs.obsidian.md/Developer+policies, and have assessed my theme's adherence to these policies.
18 | - [ ] I have read the tips in https://docs.obsidian.md/Themes/App+themes/Theme+guidelines and have self-reviewed my theme to avoid these common pitfalls.
19 | - [ ] I have added a license in the LICENSE file.
20 | - [ ] My project respects and is compatible with the original license of any code from other themes that I'm using. I have given proper attribution to these other themes in my `README.md`.
21 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Please switch to **Preview** and select one of the following links:
2 |
3 | * [Community Plugin](?template=plugin.md)
4 | * [Community Theme](?template=theme.md)
5 |
--------------------------------------------------------------------------------
/.github/workflows/plugin-stat.yml:
--------------------------------------------------------------------------------
1 | name: Pull plugin stats
2 |
3 | on:
4 | schedule:
5 | - cron: "0 0 * * *"
6 | workflow_dispatch: # Put here!!
7 |
8 | jobs:
9 | pull-stats:
10 | if: github.repository == 'obsidianmd/obsidian-releases'
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-node@v2
15 | - uses: actions/github-script@v6
16 | with:
17 | github-token: ${{ secrets.PLUGIN_STAT_TOKEN }}
18 | script: |
19 | let fs = require('fs');
20 |
21 | let plugins = JSON.parse(fs.readFileSync('./community-plugins.json', 'utf8'));
22 | let stats = JSON.parse(fs.readFileSync('./community-plugin-stats.json', 'utf8'));
23 |
24 | console.log(`Updating stats for ${Object.keys(plugins).length} plugins`);
25 | let newStats = {};
26 | for (let plugin of plugins) {
27 | let key = plugin.id;
28 | if (stats.hasOwnProperty(key)) {
29 | newStats[key] = stats[key];
30 | }
31 | }
32 |
33 | (async() => {
34 | console.log('Rate limit', (await github.rest.rateLimit.get()).data.rate);
35 | for (let key in plugins) {
36 | if (!plugins.hasOwnProperty(key)) {
37 | continue;
38 | }
39 |
40 | try {
41 | let plugin = plugins[key];
42 | let id = plugin.id;
43 |
44 | console.log(`Downloading stats for ${id} (${plugin.repo})`);
45 | let stats = newStats[id] = newStats[id] || {};
46 | let [owner, repo] = plugin.repo.split('/');
47 |
48 | const releases = [];
49 | const releasesData = github.paginate.iterator(github.rest.repos.listReleases, {owner, repo, per_page: 100});
50 | for await (const { data: data } of releasesData) {
51 | releases.push(...data);
52 | }
53 |
54 | // stats is Array<{tag_name: string, assets: Array<{name: string, download_count}>}>
55 |
56 | let updated = 0;
57 |
58 | for (let release of releases) {
59 | let version = release.tag_name;
60 | let assets = release.assets;
61 | let downloads = 0;
62 |
63 | let publishTs = new Date(release.published_at).getTime();
64 | if (publishTs > updated) {
65 | updated = publishTs;
66 | }
67 |
68 | for (let asset of assets) {
69 | if (asset.name === 'manifest.json') {
70 | downloads = asset.download_count;
71 | }
72 | }
73 | if (downloads) {
74 | stats[version] = downloads;
75 | }
76 | }
77 |
78 | let total = 0;
79 | for (let version in stats) {
80 | if (stats.hasOwnProperty(version) && version !== 'downloads' && version !== 'updated' && version !== 'latest') {
81 | total += stats[version];
82 | }
83 | }
84 |
85 | console.log(`Downloads: ${total} Releases: ${releases.length}`);
86 | stats['downloads'] = total;
87 | stats['updated'] = updated;
88 |
89 | } catch (e) {
90 | console.log('Failed', e.message);
91 | }
92 | }
93 |
94 | fs.writeFileSync('./community-plugin-stats.json', JSON.stringify(newStats, null, 2), 'utf8')
95 | console.log('All done!');
96 | })();
97 | - run: |
98 | git config --local user.name 'Obsidian Bot'
99 | git config --local user.email 'admin@obsidian.md'
100 | git add .
101 | git commit -m "chore: Update plugin stats"
102 | git push
103 |
--------------------------------------------------------------------------------
/.github/workflows/skip.yml:
--------------------------------------------------------------------------------
1 | name: "Skip on comment"
2 |
3 | on:
4 | issue_comment:
5 | types: [created]
6 |
7 | jobs:
8 | deploy:
9 | runs-on: ubuntu-latest
10 | if: github.event.issue.pull_request && contains(github.event.comment.body, '/skip') && github.event.comment.user.login != 'ObsidianReviewBot'
11 | steps:
12 | - run: gh issue edit "$NUMBER" --add-label "$LABELS"
13 | env:
14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
15 | GH_REPO: ${{ github.repository }}
16 | NUMBER: ${{ github.event.issue.number }}
17 | LABELS: Skipped code scan
18 | - run: gh issue edit "$NUMBER" --remove-assignee "$ASSIGNEE"
19 | env:
20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
21 | GH_REPO: ${{ github.repository }}
22 | NUMBER: ${{ github.event.issue.number }}
23 | ASSIGNEE: ObsidianReviewBot
24 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: 'Close stale PRs'
2 | on:
3 | schedule:
4 | - cron: "27 7 * * *"
5 |
6 | jobs:
7 | stale:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | pull-requests: write
11 | actions: write
12 | steps:
13 | - uses: actions/stale@v9
14 | with:
15 | stale-pr-message: "Hi there, as this PR has not seen any activity in the last 30 days, it will be closed in 15 days unless there are any updates."
16 | close-pr-message: "Hi there, to keep things tidy, we're closing PRs after one and a half months of inactivity.\nFeel free to create a new pull request when you're ready to continue. Thanks for your understanding!"
17 | days-before-stale: 30
18 | days-before-close: 15
19 | stale-pr-label: stale
20 | exempt-pr-labels: Ready for review,Skipped code scan
21 | operations-per-run: 200
22 |
--------------------------------------------------------------------------------
/.github/workflows/validate-plugin-entry.yml:
--------------------------------------------------------------------------------
1 | name: Validate Plugin Entry
2 |
3 | on:
4 | pull_request_target:
5 | branches:
6 | - master
7 | paths:
8 | - community-plugins.json
9 |
10 | jobs:
11 | plugin-validation:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | with:
16 | ref: "refs/pull/${{ github.event.number }}/merge"
17 | - uses: actions/setup-node@v2
18 | - uses: actions/github-script@v6
19 | with:
20 | script: |
21 | const fs = require('fs');
22 |
23 | // Don't run any validation checks if the user is just modifying existing plugin config
24 | if (context.payload.pull_request.additions <= context.payload.pull_request.deletions) {
25 | return;
26 | }
27 |
28 | const escapeHtml = (unsafe) => unsafe.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''');
29 | const errors = [];
30 | const addError = (error) => {
31 | errors.push(`:x: ${error}`);
32 | console.log('Found issue: ' + error);
33 | };
34 |
35 | const warnings = [];
36 | const addWarning = (warning) => {
37 | warnings.push(`:warning: ${warning}`);
38 | console.log('Found issue: ' + warning);
39 | }
40 |
41 | let plugin;
42 | // Core validation logic
43 | await (async () => {
44 | if (context.payload.pull_request.changed_files > 1) {
45 | addError('You modified files other than `community-plugins.json`.');
46 | }
47 |
48 | if (!context.payload.pull_request.maintainer_can_modify) {
49 | addWarning('Maintainers of this repo should be allowed to edit this pull request. This speeds up the approval process.');
50 | }
51 |
52 | if (!context.payload.pull_request.body.includes('I have added a license in the LICENSE file.')) {
53 | addError('You did not follow the pull request template');
54 | }
55 |
56 | let plugins = [];
57 | try {
58 | plugins = JSON.parse(fs.readFileSync('community-plugins.json', 'utf8'));
59 | } catch (e) {
60 | addError('Could not parse `community-plugins.json`, invalid JSON. ' + e.message);
61 | return;
62 | }
63 |
64 | plugin = plugins[plugins.length - 1];
65 |
66 | let validKeys = ['id', 'name', 'description', 'author', 'repo'];
67 | for (let key of validKeys) {
68 | if (!plugin.hasOwnProperty(key)) {
69 | addError(`Your PR does not have the required \`${key}\` property.`);
70 | }
71 | }
72 | for (let key of Object.keys(plugin)) {
73 | if (plugin.hasOwnProperty(key) && validKeys.indexOf(key) === -1) {
74 | addError(`Your PR has the invalid \`${key}\` property.`);
75 | }
76 | }
77 |
78 | // Validate plugin repo
79 | let repoInfo = plugin.repo.split('/');
80 | if (repoInfo.length !== 2) {
81 | addError(`It seems like you made a typo in the repository field \`${plugin.repo}\`.`);
82 | }
83 |
84 | let [owner, repo] = repoInfo;
85 | console.log(`Repo info: ${owner}/${repo}`);
86 |
87 | const author = context.payload.pull_request.user.login;
88 | if (owner.toLowerCase() !== author.toLowerCase()) {
89 | try {
90 | const isInOrg = await github.rest.orgs.checkMembershipForUser({org: owner, username: author});
91 | if (!isInOrg) {
92 | throw undefined;
93 | }
94 | } catch (e) {
95 | addError(`The newly added entry is not at the end, or you are submitting on someone else's behalf. The last plugin in the list is: \`${plugin.repo}\`. If you are submitting from a GitHub org, you need to be a public member of the org.`);
96 | }
97 | }
98 |
99 | try {
100 | const repository = await github.rest.repos.get({owner, repo});
101 | if (!repository.data.has_issues) {
102 | addWarning('Your repository does not have issues enabled. Users will not be able to report bugs and request features.');
103 | }
104 | } catch (e) {
105 | addError(`It seems like you made a typo in the repository field \`${plugin.repo}\`.`);
106 | }
107 |
108 | if (plugin.id?.toLowerCase().includes('obsidian')) {
109 | addError(`Please don't use the word \`obsidian\` in the plugin ID. The ID is used for your plugin's folder so keeping it short and simple avoids clutter and helps with sorting.`);
110 | }
111 | if (plugin.id?.toLowerCase().endsWith('plugin')) {
112 | addError(`Please don't use the word \`plugin\` in the plugin ID. The ID is used for your plugin's folder so keeping it short and simple avoids clutter and helps with sorting.`);
113 | }
114 | if (plugin.id && !/^[a-z0-9-_]+$/.test(plugin.id)) {
115 | addError('The plugin ID is not valid. Only alphanumeric lowercase characters and dashes are allowed.');
116 | }
117 |
118 | else if (plugin.name?.toLowerCase().includes('obsidian')) {
119 | addError(`Please don't use the word \`Obsidian\` in your plugin name since it's redundant and adds clutter to the plugin list.`);
120 | }
121 | if (plugin.name?.toLowerCase().endsWith('plugin')) {
122 | addError(`Please don't use the word \`Plugin\` in the plugin name since it's redundant and adds clutter to the plugin list.`);
123 | }
124 |
125 | if (plugin.author && /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(plugin.author)) {
126 | addWarning(`We generally discourage including your email addresses in the \`author\` field.`);
127 | }
128 |
129 | if (plugin.description?.toLowerCase().includes('obsidian')) {
130 | addError('Please don\'t include `Obsidian` in the plugin description');
131 | }
132 | if (plugin.description?.toLowerCase().includes('this plugin') || plugin.description?.toLowerCase().includes('this is a plugin') || plugin.description?.toLowerCase().includes('this plugin allows')) {
133 | addWarning('Avoid including sentences like `This is a plugin that does` in your description');
134 | }
135 |
136 | if (plugin.description?.length > 250) {
137 | addError(`Your plugin has a long description. Users typically find it difficult to read a very long description, so you should keep it short and concise.`);
138 | }
139 |
140 | if (plugin.id && plugins.filter(p => p.id === plugin.id).length > 1) {
141 | addError(`There is already a plugin with the id \`${plugin.id}\`.`);
142 | }
143 | if (plugin.name && plugins.filter(p => p.name === plugin.name).length > 1) {
144 | addError(`There is already a plugin with the name \`${plugin.name}\`.`);
145 | }
146 | if (plugin.repo && plugins.filter(p => p.repo === plugin.repo).length > 1) {
147 | addError(`There is already a entry pointing to the \`${plugin.repo}\` repository.`);
148 | }
149 |
150 | const removedPlugins = JSON.parse(fs.readFileSync('community-plugins-removed.json', 'utf8'));
151 |
152 | if (plugin.id && removedPlugins.filter(p => p.id === plugin.id).length > 1) {
153 | addError(`Another plugin used to exist with the id \`${plugin.id}\`. To avoid issues for users that still have the old plugin installed using this plugin ID is not allowed`);
154 | }
155 |
156 | if (plugin.name && removedPlugins.filter(p => p.name === plugin.name).length > 1) {
157 | addWarning(`Another plugin used to exist with the name \`${plugin.name}\`. To avoid confussion we recommend against using this name.`);
158 | }
159 |
160 | let manifest = null;
161 | try {
162 | let manifestFile = await github.rest.repos.getContent({
163 | owner,
164 | repo,
165 | path: 'manifest.json',
166 | });
167 |
168 | manifest = JSON.parse(Buffer.from(manifestFile.data.content, 'base64').toString('utf-8'));
169 | } catch (e) {
170 | addError(`You don't have a \`manifest.json\` at the root of your repo, or it could not be parsed.`);
171 | }
172 |
173 | if (manifest) {
174 | let validManifestKeys = ['id', 'name', 'description', 'author', 'version', 'minAppVersion', 'isDesktopOnly'];
175 | for (let key of validManifestKeys) {
176 | if (!manifest.hasOwnProperty(key)) {
177 | addError(`Your manifest does not have the required \`${key}\` property.`);
178 | }
179 | }
180 |
181 | if (plugin.name && manifest.id !== plugin.id) {
182 | addError(`Plugin ID mismatch, the ID in this PR (\`${plugin.id}\`) is not the same as the one in your repo (\`${manifest.id}\`). If you just changed your plugin ID, remember to change it in the manifest.json in your repo and your latest GitHub release.`);
183 | }
184 | if (plugin.name && manifest.name !== plugin.name) {
185 | addError(`Plugin name mismatch, the name in this PR (\`${plugin.name}\`) is not the same as the one in your repo (\`${manifest.name}\`). If you just changed your plugin name, remember to change it in the manifest.json in your repo and your latest GitHub release.`);
186 | }
187 |
188 | if (manifest.authorUrl) {
189 | if (manifest.authorUrl === "https://obsidian.md") {
190 | addError(`The \`authorUrl\` field in your manifest should not point to the Obsidian Website. If you don't have a website you can just point it to your GitHub profile.`);
191 | }
192 |
193 | if (manifest.authorUrl.toLowerCase().includes("github.com/" + plugin.repo.toLowerCase())) {
194 | addError(`The \`authorUrl\` field in your manifest should not point to the GitHub repository of the plugin.`);
195 | }
196 | }
197 |
198 | if (manifest.fundingUrl && manifest.fundingUrl === "https://obsidian.md/pricing") {
199 | addError(`The \`fundingUrl\` field in your manifest should not point to the Obsidian Website, If you don't have a link were users can donate to you, you can just remove it from the manifest.`);
200 | }
201 | if (manifest.fundingUrl && manifest.fundingUrl === "") {
202 | addError('The `fundingUrl` is meant for links to services like _Buy me a coffee_, _GitHub sponsors_ and so on, if you don\'t have such a link remove it from the manifest.');
203 | }
204 |
205 | if (!/^[0-9.]+$/.test(manifest.version)) {
206 | addError('Your latest version number is not valid. Only numbers and dots are allowed.');
207 | }
208 |
209 | try {
210 | let release = await github.rest.repos.getReleaseByTag({
211 | owner,
212 | repo,
213 | tag: manifest.version,
214 | });
215 |
216 | const assets = release.data.assets || [];
217 | if (!assets.find(p => p.name === 'main.js')) {
218 | addError('Your latest Release is missing the `main.js` file.');
219 | }
220 | if (!assets.find(p => p.name === 'manifest.json')) {
221 | addError('Your latest Release is missing the `manifest.json` file.');
222 | }
223 | } catch (e) {
224 | addError(`Unable to find a release with the tag \`${manifest.version}\`. Make sure that the version in your manifest.json file in your repo points to the correct Github Release.`);
225 | }
226 |
227 | }
228 |
229 | try {
230 | await github.rest.licenses.getForRepo({owner, repo});
231 | } catch (e) {
232 | addWarning(`Your repository does not include a license. It is generally recommended for open-source projects to have a license. Go to to compare different open source licenses.`);
233 | }
234 | })();
235 |
236 | if (errors.length > 0 || warnings.length > 0) {
237 | let message = [`#### Hello!\n`]
238 | message.push(`**I found the following issues in your plugin submission**\n`);
239 |
240 | if (errors.length > 0) {
241 | message.push(`**Errors:**\n`);
242 | message = message.concat(errors);
243 | message.push(`\n---\n`);
244 | }
245 | if (warnings.length > 0) {
246 | message.push(`**Warnings:**\n`);
247 | message = message.concat(warnings);
248 | message.push(`\n---\n`);
249 | }
250 |
251 | message.push(`This check was done automatically. Do NOT open a new PR for re-validation. Instead, to trigger this check again, make a change to your PR and wait a few minutes, or close and re-open it.`);
252 |
253 | await github.rest.issues.createComment({
254 | issue_number: context.issue.number,
255 | owner: context.repo.owner,
256 | repo: context.repo.repo,
257 | body: message.join('\n'),
258 | });
259 | }
260 | const labels = [];
261 |
262 | if (errors.length > 0) {
263 | labels.push("Validation failed");
264 | core.setFailed("Failed to validate plugin");
265 | }
266 |
267 | if (errors.length === 0) {
268 | await github.rest.pulls.update({
269 | owner: context.repo.owner,
270 | repo: context.repo.repo,
271 | pull_number: context.issue.number,
272 | title: `Add plugin: ${plugin.name}`
273 | });
274 |
275 | const comments = github.rest.issues.listComments({
276 | owner: context.repo.owner,
277 | repo: context.repo.repo,
278 | issue_number: context.issue.number
279 | });
280 | const commentAuthors = [];
281 | for (const comment in comments) {
282 | commentAuthors.push(comment.user.login);
283 | }
284 |
285 | if (!commentAuthors.includes("ObsidianReviewBot")) {
286 | await github.rest.issues.addAssignees({
287 | owner: context.repo.owner,
288 | repo: context.repo.repo,
289 | issue_number: context.issue.number,
290 | assignees: 'ObsidianReviewBot'
291 | });
292 | }
293 |
294 | if(!context.payload.pull_request.labels.filter(label => label.name === 'Changes requested').length > 0) {
295 | labels.push("Ready for review");
296 | }
297 | }
298 | if (context.payload.pull_request.labels.filter(label => label.name === 'Changes requested').length > 0) {
299 | labels.push('Changes requested');
300 | }
301 | if (context.payload.pull_request.labels.filter(label => label.name === 'Additional review required').length > 0) {
302 | labels.push('Additional review required');
303 | }
304 | if (context.payload.pull_request.labels.filter(label => label.name === 'Minor changes requested').length > 0) {
305 | labels.push('Minor changes requested');
306 | }
307 | if (context.payload.pull_request.labels.filter(label => label.name === 'requires author rebase').length > 0) {
308 | labels.push('requires author rebase');
309 | }
310 | if (context.payload.pull_request.labels.filter(label => label.name === 'Installation not recommended').length > 0) {
311 | labels.push('Installation not recommended');
312 | }
313 | if (context.payload.pull_request.labels.filter(label => label.name === 'Changes made').length > 0) {
314 | labels.push('Changes made');
315 | }
316 | if (context.payload.pull_request.labels.filter(label => label.name === 'Skipped code scan').length > 0) {
317 | labels.push('Skipped code scan');
318 | }
319 | labels.push('plugin');
320 |
321 | await github.rest.issues.setLabels({
322 | issue_number: context.issue.number,
323 | owner: context.repo.owner,
324 | repo: context.repo.repo,
325 | labels,
326 | });
327 | permissions:
328 | contents: read
329 | issues: write
330 | pull-requests: write
331 |
--------------------------------------------------------------------------------
/.github/workflows/validate-theme-entry.yml:
--------------------------------------------------------------------------------
1 | name: Validate Theme Entry
2 |
3 | on:
4 | pull_request_target:
5 | branches:
6 | - master
7 | paths:
8 | - community-css-themes.json
9 |
10 | jobs:
11 | theme-validation:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | with:
16 | ref: "refs/pull/${{ github.event.number }}/merge"
17 | - uses: actions/setup-node@v2
18 | - run: npm install -g probe-image-size && npm link probe-image-size
19 | - uses: actions/github-script@v6
20 | with:
21 | script: |
22 | if (context.payload.pull_request.additions <= context.payload.pull_request.deletions) {
23 | // Don't run any validation checks if the user is just modifying existing theme config
24 | return;
25 | }
26 |
27 | const probe = require('probe-image-size');
28 | const fs = require('fs');
29 |
30 | const errors = [];
31 | const addError = (error) => {
32 | errors.push(`:x: ${error}`);
33 | console.log('Found issue: ' + error);
34 | };
35 |
36 | const warnings = [];
37 | const addWarning = (warning) => {
38 | warnings.push(`:warning: ${warning}`);
39 | console.log('Found issue: ' + warning);
40 | }
41 |
42 | // core validation logic
43 | await (async () => {
44 | if (context.payload.pull_request.changed_files > 1) {
45 | addError('You modified files other than `community-css-themes.json`.');
46 | }
47 |
48 | if (!context.payload.pull_request.maintainer_can_modify) {
49 | addWarning('Maintainers of this repo should be allowed to edit this pull request. This speeds up the approval process.');
50 | }
51 |
52 | if (context.payload.pull_request.body.includes('Please switch to **Preview** and select one of the following links:')) {
53 | addError('You did not follow the pull request template.');
54 | }
55 |
56 | let themes;
57 |
58 | try {
59 | themes = JSON.parse(fs.readFileSync('community-css-themes.json', 'utf8'));
60 | } catch (e) {
61 | addError('Could not parse `community-css-themes.json`, invalid JSON. ' + e.message);
62 | return;
63 | }
64 | const theme = themes[themes.length - 1];
65 |
66 | const validPrKeys = ['name', 'author', 'repo', 'screenshot', 'modes'];
67 | for (let key of validPrKeys) {
68 | if (!theme.hasOwnProperty(key)) {
69 | addError(`Your PR does not have the required \`${key}\` property.`);
70 | }
71 | }
72 | for (let key of Object.keys(theme)) {
73 | if (!validPrKeys.includes(key)) {
74 | addError(`Your PR has the invalid \`${key}\` property.`);
75 | }
76 | }
77 |
78 | // Validate theme repo
79 | let repoInfo = theme.repo.split('/');
80 | if (repoInfo.length !== 2) {
81 | addError(`It seems like you made a typo in the repository field ${theme.repo}`);
82 | return;
83 | }
84 |
85 | let [owner, repo] = repoInfo;
86 | console.log(`Repo info: ${owner}/${repo}`);
87 |
88 | const author = context.payload.pull_request.user.login;
89 | if (owner.toLowerCase() !== author.toLowerCase()) {
90 | try {
91 | const isInOrg = await github.rest.orgs.checkMembershipForUser({ org: owner, username: author });
92 | if (!isInOrg) {
93 | throw undefined;
94 | }
95 | } catch (e) {
96 | addError(`The newly added entry is not at the end, or you are submitting on someone else's behalf. The last theme in the list is: \`${theme.repo}\`. If you are submitting from a GitHub org, you need to be a public member of the org.`);
97 | }
98 | }
99 | try {
100 | const repository = await github.rest.repos.get({ owner, repo });
101 | if (!repository.data.has_issues) {
102 | addWarning('Your repository does not have issues enabled. Users will not be able to report bugs and request features.');
103 | }
104 | } catch (e) {
105 | addError(`It seems like you made a typo in the repository field ${theme.repo}`);
106 | return;
107 | }
108 |
109 | if (theme.name.toLowerCase().includes('obsidian')) {
110 | addError(`We discourage themes from including the word \`Obsidian\` in their name since it's redundant and makes the theme selection screen harder to visually parse.`);
111 | }
112 | if (theme.name.toLowerCase().includes('theme')) {
113 | addError(`We discourage themes from including the word \`theme\` in their name since it's redundant and makes the theme selection screen harder to visually parse.`);
114 | }
115 |
116 | if (/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$/.test(theme.author)) {
117 | addWarning('We generally discourage from including email addresses in the `author` field.');
118 | }
119 |
120 | if (themes.filter(t => t.name === theme.name).length > 1) {
121 | addError('There is already a theme with this name');
122 | }
123 |
124 | if (themes.filter(t => t.repo === theme.repo).length > 1) {
125 | addError('There is already a entry pointing to the `' + theme.repo + '` repository');
126 | }
127 |
128 | let manifest = null;
129 |
130 | try {
131 | let manifestFile = await github.rest.repos.getContent({
132 | owner,
133 | repo,
134 | path: 'manifest.json',
135 | });
136 |
137 | manifest = JSON.parse(Buffer.from(manifestFile.data.content, 'base64').toString('utf-8'));
138 | } catch (e) {
139 | addError(`You don't have a valid \`manifest.json\` at the root of your repo.`);
140 | }
141 |
142 | if (manifest) {
143 | let requiredManifestKeys = ['name', 'minAppVersion', 'author', 'version'];
144 |
145 | for (let key of requiredManifestKeys) {
146 | if (!manifest.hasOwnProperty(key)) {
147 | addError(`Your manifest does not have the required \`${key}\` property.`);
148 | }
149 | }
150 |
151 | if (manifest.name !== theme.name) {
152 | addError(`Theme name mismatch, the name in this PR (\`${theme.name}\`) is not the same as the one in your repo (\`${manifest.name}\`). If you just changed your theme name, remember to change it in the manifest.json in your repo, and your latest GitHub release, if you have one.`);
153 | }
154 | if (manifest.authorUrl) {
155 | if (manifest.authorUrl === "https://obsidian.md") {
156 | addError(`The \`authorUrl\` field in your manifest should not point to the Obsidian Website. If you don't have a website you can just point it to your GitHub profile.`);
157 | }
158 |
159 | if (manifest.authorUrl.toLowerCase().includes("github.com/" + theme.repo.toLowerCase())) {
160 | addError(`The \`authorUrl\` field in your manifest should not point to the GitHub repository of the theme.`);
161 | }
162 | }
163 |
164 | if (manifest.fundingUrl && manifest.fundingUrl === "https://obsidian.md/pricing") {
165 | addError(`The \`fundingUrl\` field in your manifest should not point to the Obsidian Website, If you don't have a link were users can donate to you, you can just omit this.`);
166 | }
167 |
168 | if (!(/^[0-9.]+$/i.test(manifest.version))) {
169 | addError('Your latest version number is not valid. Only numbers and dots are allowed.');
170 | }
171 |
172 | try {
173 | await github.rest.repos.getContent({
174 | owner, repo, path: 'theme.css'
175 | });
176 | } catch (e) {
177 | addError('Your repository does not include a `theme.css` file');
178 | }
179 |
180 | try {
181 | await github.rest.repos.getContent({
182 | owner, repo, path: 'obsidian.css'
183 | });
184 | addWarning('Your repository includes a `obsidian.css` file, this is only used in legacy versions of Obsidian.');
185 | } catch (e) { }
186 |
187 | let imageMeta = null;
188 | try {
189 | const screenshot = await github.rest.repos.getContent({
190 | owner, repo, path: theme.screenshot
191 | });
192 | imageMeta = await probe(screenshot.data.download_url);
193 | } catch (e) {
194 | console.log(e);
195 | addError('The theme screenshot cannot be found.');
196 | }
197 | if (imageMeta) {
198 | if (imageMeta.type !== 'png' && imageMeta.type !== 'jpg') {
199 | addError('Theme screenshot is not of filetype `.png` or `.jpg`');
200 | }
201 | const recommendedSize = `we generally recommend a size around 512 × 288 pixels.\n Detected size: ${imageMeta.width} x ${imageMeta.height} pixels`
202 | if (imageMeta.width > 1000 || imageMeta.height > 500) {
203 | addError(`Your theme screenshot is too big, ${recommendedSize}`);
204 | }
205 | else if (imageMeta.width < 250 || imageMeta.height < 100) {
206 | addError(`Your theme screenshot is too small, ${recommendedSize}`);
207 | } else if (imageMeta.width !==512 || imageMeta.height !== 288) {
208 | addWarning(`Theme theme screenshot size is not optimal, ${recommendedSize}`);
209 | }
210 | }
211 |
212 |
213 | // only validate releases if version is included
214 | if (manifest.hasOwnProperty('version')) {
215 | try {
216 | let release = await github.rest.repos.getReleaseByTag({
217 | owner,
218 | repo,
219 | tag: manifest.version,
220 | });
221 |
222 | const assets = release.data.assets || [];
223 | if (!assets.find(p => p.name === 'theme.css')) {
224 | addError('Your latest Release is missing the `theme.css` file.');
225 | }
226 | if (!assets.find(p => p.name === 'manifest.json')) {
227 | addError('Your latest Release is missing the `manifest.json` file.');
228 | }
229 | } catch (e) { }
230 |
231 | }
232 | }
233 |
234 | try {
235 | await github.rest.licenses.getForRepo({ owner, repo });
236 | } catch (e) {
237 | addWarning('Your repository does not include a license. It is generally recommended for open-source projects to have a license. Go to to compare different open source licenses.');
238 | }
239 |
240 | await github.rest.pulls.update({
241 | owner: context.repo.owner,
242 | repo: context.repo.repo,
243 | pull_number: context.issue.number,
244 | title: `Add theme: ${theme.name}`
245 | });
246 |
247 | })();
248 |
249 |
250 | if (errors.length > 0 || warnings.length > 0) {
251 | let message = [`#### Hello!\n`];
252 | message.push(`**I found the following issues in your theme submission**\n`);
253 | if (errors.length > 0) {
254 | message.push(`**Errors:**\n`);
255 | message = message.concat(errors);
256 | message.push(`\n---\n`);
257 | }
258 | if (warnings.length > 0) {
259 | message.push(`**Warnings:**\n`);
260 | message = message.concat(warnings);
261 | message.push(`\n---\n`);
262 | }
263 | message.push(`This check was done automatically. Do NOT open a new PR for re-validation. Instead, to trigger this check again, make a change to your PR and wait a few minutes, or close and re-open it.`);
264 |
265 |
266 | await github.rest.issues.createComment({
267 | issue_number: context.issue.number,
268 | owner: context.repo.owner,
269 | repo: context.repo.repo,
270 | body: message.join('\n'),
271 | });
272 | }
273 | if (errors.length > 0) {
274 | core.setFailed("Failed to validate theme");
275 | }
276 |
277 | let labels = errors.length > 0 ? ['Validation failed'] : ['Ready for review'];
278 | if (context.payload.pull_request.labels.includes('Changes requested')) {
279 | labels.push('Changes requested');
280 | }
281 | if (context.payload.pull_request.labels.includes('Additional review required')) {
282 | labels.push('Additional review required');
283 | }
284 | if (context.payload.pull_request.labels.includes('Minor changes requested')) {
285 | labels.push('Minor changes requested');
286 | }
287 | if (context.payload.pull_request.labels.includes('Requires author rebase')) {
288 | labels.push('requires author rebase');
289 | }
290 | if (context.payload.pull_request.labels.includes('Installation not recommended')) {
291 | labels.push('Installation not recommended');
292 | }
293 | if (context.payload.pull_request.labels.includes('Changes made')) {
294 | labels.push('Changes made');
295 | }
296 | labels.push('theme');
297 |
298 | await github.rest.issues.setLabels({
299 | issue_number: context.issue.number,
300 | owner: context.repo.owner,
301 | repo: context.repo.repo,
302 | labels,
303 | });
304 | permissions:
305 | contents: read
306 | issues: write
307 | pull-requests: write
308 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .DS_Store
3 | .idea
4 | *.iml
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## About this repo
2 |
3 | This repo is used for hosting public releases of Obsidian, as well as our community plugins & themes directories.
4 |
5 | Obsidian is not open source software and this repo _DOES NOT_ contain the source code of Obsidian. However, if you wish to contribute to Obsidian, you can easily do so with our extensive plugin system. A plugin guide can be found here: https://docs.obsidian.md
6 |
7 | This repo does not accept issues, if you have questions or issues with plugins, please go to their own repo to file them. If you have questions or issues about core Obsidian itself, please post them to our community: https://obsidian.md/community
8 |
9 | ## Submit your plugin or theme
10 |
11 | When opening a pull request, please switch to preview mode and select the option to go through our submission checklist. Submit your entry by following the convention in the JSON file and we will review your submission.
12 |
13 | Thanks for submitting your creations!
14 |
15 | You can find a detailed explanation for submitting your [plugin here](https://docs.obsidian.md/Plugins/Releasing/Submit+your+plugin) and your [theme here](https://docs.obsidian.md/Themes/App+themes/Submit+your+theme).
16 |
17 | ## Policies
18 |
19 | All submissions must conform with our [developer policies](https://docs.obsidian.md/Developer+policies)
20 |
21 | ## Community Theme
22 |
23 | To add your theme to our theme store, make a pull request to the `community-css-theme.json` file. Please add your theme to the end of the list.
24 |
25 | - `name`: a unique name for your theme. Must not collide with other themes.
26 | - `author`: the author's name for display.
27 | - `repo`: the GitHub repository identifier, in the form of `user-name/repo-name`, if your GitHub repo is located at `https://github.com/user-name/repo-name`.
28 | - `screenshot`: path to the screenshot of your theme.
29 | - `modes`: if your theme supports both dark and light mode, put `["dark", "light"]`. Otherwise, put `["dark"]` if your theme only supports dark mode, or `["light"]` if your theme only supports light mode.
30 | - `publish`: if your theme supports Obsidian Publish, set this to `true`. Omit it otherwise.
31 |
32 | To get your theme compatible with Obsidian Publish, you can use `applyCss` and `applyCssByLink` to test out your CSS in the developer console of Obsidian Publish sites, so that you don't actually need to own sites to test your `publish.css`. You can test it out on our help site here: https://help.obsidian.md/
33 |
34 | `applyCss` takes a CSS string, you can use backtick (template strings) for multiline CSS. `applyCssByLink` takes a link and loads the CSS, would recommend GitHub raw file URLs.
35 |
36 | ## Community Plugin
37 |
38 | ### Community Plugins format
39 |
40 | To add your plugin to the list, make a pull request to the `community-plugins.json` file. Please add your plugin to the end of the list.
41 |
42 | - `id`: A unique ID for your plugin. Make sure this is the same one you have in your `manifest.json`.
43 | - `name`: The name of your plugin.
44 | - `author`: The author's name.
45 | - `description`: A short description of what your plugin does.
46 | - `repo`: The GitHub repository identifier, in the form of `user-name/repo-name`, if your GitHub repo is located at `https://github.com/user-name/repo-name`.
47 |
48 | ### How community plugins are pulled
49 |
50 | - Obsidian will read the list of plugins in `community-plugins.json`.
51 | - The `name`, `author` and `description` fields are used for searching.
52 | - When the user opens the detail page of your plugin, Obsidian will pull the `manifest.json` and `README.md` from your GitHub repo).
53 | - The `manifest.json` in your repo will only be used to figure out the latest version. Actual files are fetched from your GitHub releases.
54 | - If your `manifest.json` requires a version of Obsidian that's higher than the running app, your `versions.json` will be consulted to find the latest version of your plugin that is compatible.
55 | - When the user chooses to install your plugin, Obsidian will look for your GitHub releases tagged identically to the version inside `manifest.json`.
56 | - Obsidian will download `manifest.json`, `main.js`, and `styles.css` (if available), and store them in the proper location inside the vault.
57 |
58 | ### Announcing the First Public Release of your Plugin/Theme
59 |
60 | - Once admitted to the plugin/theme browser, you can announce the public availability of your plugin/theme:
61 | - [in the forums](https://forum.obsidian.md/c/share-showcase/9) as a showcase, and
62 | - [on the Discord Server](https://discord.gg/veuWUTm) in the channel `#updates`. (You need the `developer` role to be able to post in that channel; [you can get that role here](https://discord.com/channels/686053708261228577/702717892533157999/830492034807758859).)
63 | - You can also announce the first working version of your plugin as a public beta before "officially" submitting it to the plugin/theme browser. That way, you can acquire some beta testers for feedback. It's recommended to use the [BRAT Plugin](https://obsidian.md/plugins?id=obsidian42-brat) to make the installation as easy as possible for interested beta testers.
64 |
--------------------------------------------------------------------------------
/cla.md:
--------------------------------------------------------------------------------
1 | # Contributor License Agreement ("Agreement")
2 |
3 | Thank you for your interest in the open source project(s) managed by Dynalist, Inc. (“Obsidian”). By contributing to Obsidian, you accept and agree to the following terms and conditions for your present and future contributions submitted to Obsidian. Except for the license granted herein to Obsidian and recipients of software distributed by Obsidian, you reserve all right, title, and interest in and to your contributions.
4 |
5 | 1. Definitions.
6 | "You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Obsidian. For legal entities, the entity making a contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
7 |
8 | 2. Grant of Copyright License.
9 | Subject to the terms and conditions of this Agreement, You hereby grant to Obsidian and to recipients of software distributed by Obsidian a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your contributions and such derivative works.
10 |
11 | 3. Grant of Patent License.
12 | Subject to the terms and conditions of this Agreement, You hereby grant to Obsidian and to recipients of software distributed by Obsidian a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your contribution(s) alone or by combination of Your contribution(s) with the Work to which such contribution(s) were submitted.
13 |
14 | 4. You represent that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your contributions, you represent that you have received permission to make contributions on behalf of that employer, that your employer has waived such rights for your contributions to Obsidian, or that your employer has executed a separate Corporate CLA with Obsidian.
15 |
16 | 5. You represent that each of Your contributions is Your original creation (see Section 7 for submissions on behalf of others). You represent that Your contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which you are personally aware and which are associated with any part of Your contributions.
17 |
18 | 6. You are not expected to provide support for Your contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
19 |
20 | 7. Should You wish to submit work that is not Your original creation, You may submit it to Obsidian separately from any contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, copyrights, and trademarks) of which you are personally aware, and conspicuously marking the work as "Submitted on behalf of a third-party: [named here]".
21 |
22 | 8. You agree to notify Obsidian of any facts or circumstances of which you become aware that would make these representations inaccurate in any respect.
23 |
24 | Please reply "I have read the CLA agreement and I hereby sign the CLA" to indicate your agreement to the terms of this Agreement.
25 |
--------------------------------------------------------------------------------
/community-css-themes-removed.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Creature",
4 | "reason": "Repository archived"
5 | }
6 | ]
--------------------------------------------------------------------------------
/community-css-themes.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Atom",
4 | "author": "kognise",
5 | "repo": "kognise/obsidian-atom",
6 | "screenshot": "screenshot-hybrid.png",
7 | "modes": ["dark", "light"]
8 | },
9 | {
10 | "name": "Amethyst",
11 | "author": "cotemaxime",
12 | "repo": "cotemaxime/obsidian-amethyst",
13 | "screenshot": "screenshot.png",
14 | "modes": ["dark", "light"],
15 | "legacy": true
16 | },
17 | {
18 | "name": "Obsidian gruvbox",
19 | "author": "insanum",
20 | "repo": "insanum/obsidian_gruvbox",
21 | "screenshot": "dark.png",
22 | "modes": ["dark", "light"]
23 | },
24 | {
25 | "name": "Obsidian Nord",
26 | "author": "insanum",
27 | "repo": "insanum/obsidian_nord",
28 | "screenshot": "dark.png",
29 | "modes": ["dark", "light"]
30 | },
31 | {
32 | "name": "Dracula for Obsidian",
33 | "author": "jarodise",
34 | "repo": "jarodise/Dracula-for-Obsidian.md",
35 | "screenshot": "screencap.jpg",
36 | "modes": ["dark"]
37 | },
38 | {
39 | "name": "Gastown",
40 | "author": "lizardmenfromspace",
41 | "repo": "dogwaddle/obsidian-gastown-theme.md",
42 | "screenshot": "ObsidianOne.png",
43 | "modes": ["light"],
44 | "legacy": true
45 | },
46 | {
47 | "name": "80s Neon",
48 | "author": "deathau",
49 | "repo": "deathau/80s-Neon-for-Obsidian.md",
50 | "screenshot": "screenshot.jpg",
51 | "modes": ["dark", "light"]
52 | },
53 | {
54 | "name": "Base2Tone",
55 | "author": "deathau",
56 | "repo": "deathau/Base2Tone-For-Obsidian.md",
57 | "screenshot": "colours.gif",
58 | "modes": ["dark"]
59 | },
60 | {
61 | "name": "Notation",
62 | "author": "deathau",
63 | "repo": "deathau/Notation-for-Obsidian",
64 | "screenshot": "screenshot.jpg",
65 | "modes": ["dark", "light"]
66 | },
67 | {
68 | "name": "Christmas",
69 | "author": "deathau",
70 | "repo": "deathau/obsidian-christmas-theme",
71 | "screenshot": "screenshot.png",
72 | "modes": ["dark", "light"]
73 | },
74 | {
75 | "name": "Solarized",
76 | "author": "harmtemolder",
77 | "repo": "harmtemolder/obsidian-solarized",
78 | "screenshot": "screenshot.png",
79 | "modes": ["dark", "light"]
80 | },
81 | {
82 | "name": "Comfort color dark",
83 | "author": "ezs",
84 | "repo": "obsidian-ezs/obsidian-comfort-color-dark",
85 | "screenshot": "screencap.png",
86 | "modes": ["dark"]
87 | },
88 | {
89 | "name": "Ursa",
90 | "author": "ezs",
91 | "repo": "obsidian-ezs/obsidian-ursa",
92 | "screenshot": "light-theme_full.png",
93 | "modes": ["dark", "light"]
94 | },
95 | {
96 | "name": "Cybertron",
97 | "author": "nickmilo",
98 | "repo": "nickmilo/Cybertron",
99 | "screenshot": "Cybertron.png",
100 | "modes": ["dark"]
101 | },
102 | {
103 | "name": "Moonlight",
104 | "author": "karz",
105 | "repo": "kartik-karz/moonlight-obsidian",
106 | "screenshot": "moonlight-theme.png",
107 | "modes": ["dark", "light"]
108 | },
109 | {
110 | "name": "Red Graphite",
111 | "author": "SeanWcom",
112 | "repo": "seanwcom/Red-Graphite-for-Obsidian",
113 | "screenshot": "thumbnail.png",
114 | "modes": ["dark", "light"]
115 | },
116 | {
117 | "name": "Subtlegold",
118 | "author": "karz",
119 | "repo": "kartik-karz/subtlegold-obsidian",
120 | "screenshot": "subtlegold-theme.png",
121 | "modes": ["dark", "light"]
122 | },
123 | {
124 | "name": "Obsidian Boom",
125 | "author": "Sainadh",
126 | "repo": "sainadh-d/obsidian-boom",
127 | "screenshot": "roam-1.png",
128 | "modes": ["light"]
129 | },
130 | {
131 | "name": "Hulk",
132 | "author": "Reggie",
133 | "repo": "pgalliford/Obsidian-theme-Incredible-Hulk",
134 | "screenshot": "Screen Shot.png",
135 | "modes": ["dark"]
136 | },
137 | {
138 | "name": "Pisum",
139 | "author": "MooddooM",
140 | "repo": "GuangluWu/obsidian-pisum",
141 | "screenshot": "fullpower.png",
142 | "modes": ["dark"]
143 | },
144 | {
145 | "name": "Traffic Lights",
146 | "author": "Boyd",
147 | "repo": "elliotboyd/obsidian-traffic-lights",
148 | "screenshot": "dark.png",
149 | "modes": ["dark", "light"],
150 | "legacy": true
151 | },
152 | {
153 | "name": "Ars Magna",
154 | "author": "Mediapathic",
155 | "repo": "mediapathic/obsidian-arsmagna-theme",
156 | "screenshot": "arsmagna.png",
157 | "modes": ["dark", "light"],
158 | "legacy": true
159 | },
160 | {
161 | "name": "Obsdn-Dark-Rmx",
162 | "author": "_ph",
163 | "repo": "cannibalox/Obsdn-dark-rmx",
164 | "screenshot": "Obsdn-Dark-Rmx.png",
165 | "modes": ["dark", "light"],
166 | "legacy": true
167 | },
168 | {
169 | "name": "Minimal",
170 | "author": "kepano",
171 | "repo": "kepano/obsidian-minimal",
172 | "screenshot": "dark-simple.png",
173 | "modes": ["dark", "light"]
174 | },
175 | {
176 | "name": "obsidian_ia",
177 | "author": "rcvd",
178 | "repo": "rcvd/obsidian_ia",
179 | "screenshot": "light.png",
180 | "modes": ["dark", "light"]
181 | },
182 | {
183 | "name": "Charcoal",
184 | "author": "bernardo_v",
185 | "repo": "bcdavasconcelos/Obsidian-Charcoal",
186 | "screenshot": "charcoal.png",
187 | "modes": ["dark"]
188 | },
189 | {
190 | "name": "Panic Mode",
191 | "author": "bernardo_v",
192 | "repo": "bcdavasconcelos/Obsidian-Panic_Mode",
193 | "screenshot": "panic.png",
194 | "modes": ["dark"]
195 | },
196 | {
197 | "name": "Dark Graphite",
198 | "author": "bernardo_v",
199 | "repo": "bcdavasconcelos/Obsidian-Graphite",
200 | "screenshot": "graphite.png",
201 | "modes": ["dark"]
202 | },
203 | {
204 | "name": "Ayu",
205 | "author": "bernardo_v",
206 | "repo": "bcdavasconcelos/Obsidian-Ayu",
207 | "screenshot": "ayu2.png",
208 | "modes": ["light"],
209 | "legacy": true
210 | },
211 | {
212 | "name": "Ayu Mirage",
213 | "author": "bernardo_v",
214 | "repo": "bcdavasconcelos/Obsidian-Ayu_Mirage",
215 | "screenshot": "ayu1.png",
216 | "modes": ["dark"]
217 | },
218 | {
219 | "name": "GDCT",
220 | "author": "bernardo_v",
221 | "repo": "bcdavasconcelos/Obsidian-GDCT",
222 | "screenshot": "gdct.png",
223 | "modes": ["light"]
224 | },
225 | {
226 | "name": "GDCT Dark",
227 | "author": "bernardo_v",
228 | "repo": "bcdavasconcelos/Obsidian-GDCT_Dark",
229 | "screenshot": "gdct.png",
230 | "modes": ["dark"]
231 | },
232 | {
233 | "name": "Obuntu",
234 | "author": "Dubinin Dmitry",
235 | "repo": "DubininDmitry/Obuntu-theme-for-Obsidian",
236 | "screenshot": "screenshot.jpg",
237 | "modes": ["dark", "light"]
238 | },
239 | {
240 | "name": "Ono Sendai",
241 | "author": "_ph",
242 | "repo": "cannibalox/ono-sendai_obsdn",
243 | "screenshot": "ono-sendai_obsdn_00.png",
244 | "modes": ["dark", "light"]
245 | },
246 | {
247 | "name": "Blue Topaz",
248 | "author": "Whyl",
249 | "repo": "whyt-byte/Blue-Topaz_Obsidian-css",
250 | "screenshot": "preview_Blue Topaz.png",
251 | "modes": ["dark", "light"]
252 | },
253 | {
254 | "name": "Reverie",
255 | "author": "Santi Younger",
256 | "repo": "santiyounger/Reverie-Obsidian-Theme",
257 | "screenshot": "img/reverie-2020-09-14-dark.png",
258 | "modes": ["dark", "light"]
259 | },
260 | {
261 | "name": "Dark Graphite Pie",
262 | "author": "kitchenrunner",
263 | "repo": "ryjjin/Obsidian-Dark-Graphite-Pie-theme",
264 | "screenshot": "Dark Graphite Pie theme 0.9.4.png",
265 | "modes": ["dark", "light"]
266 | },
267 | {
268 | "name": "Obsidianite",
269 | "author": "Benny Guo",
270 | "repo": "bennyxguo/Obsidian-Obsidianite",
271 | "screenshot": "images/demo1.png",
272 | "modes": ["dark"]
273 | },
274 | {
275 | "name": "Gitsidian",
276 | "author": "Ish Gunacar",
277 | "repo": "ishgunacar/gitsidian",
278 | "screenshot": "showcase.png",
279 | "modes": ["dark", "light"]
280 | },
281 | {
282 | "name": "Comfort Smooth",
283 | "author": "Spark",
284 | "repo": "sparklau/comfort-smooth",
285 | "screenshot": "comfort-smooth.png",
286 | "modes": ["dark"]
287 | },
288 | {
289 | "name": "Suddha",
290 | "author": "dxcore35",
291 | "repo": "dxcore35/Suddha-theme",
292 | "screenshot": "Images/Preview1.jpg",
293 | "modes": ["dark", "light"]
294 | },
295 | {
296 | "name": "Discordian",
297 | "author": "radekkozak",
298 | "repo": "radekkozak/discordian",
299 | "screenshot": "media/screenshots/discordian-full-mode.png",
300 | "modes": ["dark"]
301 | },
302 | {
303 | "name": "Al Dente",
304 | "author": "chad-bennett",
305 | "repo": "chad-bennett/al-dente-obsidian-theme",
306 | "screenshot": "aldente-screenshot.png",
307 | "modes": ["light"]
308 | },
309 | {
310 | "name": "Wasp",
311 | "author": "Santi Younger",
312 | "repo": "santiyounger/Wasp-Obsidian-Theme",
313 | "screenshot": "img/wasp-dark.png",
314 | "modes": ["dark", "light"]
315 | },
316 | {
317 | "name": "Higlighter",
318 | "author": "lukauskas",
319 | "repo": "lukauskas/obsidian-highlighter-theme",
320 | "screenshot": "screenshots/screenshot-themes-panel.png",
321 | "modes": ["light"],
322 | "legacy": true
323 | },
324 | {
325 | "name": "ITS Theme",
326 | "author": "SlRvb",
327 | "repo": "SlRvb/Obsidian--ITS-Theme",
328 | "screenshot": "ITS.png",
329 | "modes": ["dark", "light"]
330 | },
331 | {
332 | "name": "Spectrum",
333 | "author": "Wiktoria Mielcarek",
334 | "repo": "Braweria/Spectrum",
335 | "screenshot": "SpectrumPreview.png",
336 | "modes": ["dark", "light"]
337 | },
338 | {
339 | "name": "Hipstersmoothie",
340 | "author": "Andrew Lisowski",
341 | "repo": "hipstersmoothie/hipstersmoothie-obsidian-theme",
342 | "screenshot": "hipstersmoothie-obsidian-theme.png",
343 | "modes": ["dark"]
344 | },
345 | {
346 | "name": "Aurora",
347 | "author": "Benny Guo",
348 | "repo": "auroral-ui/aurora-obsidian-md",
349 | "screenshot": "screenshots/screenshot-1.png",
350 | "modes": ["dark"]
351 | },
352 | {
353 | "name": "Iceberg",
354 | "author": "izumin5210",
355 | "repo": "izumin5210/obsidian-iceberg",
356 | "screenshot": "screenshot.png",
357 | "modes": ["dark"]
358 | },
359 | {
360 | "name": "Yin and Yang",
361 | "author": "Chetachi E.",
362 | "repo": "chetachiezikeuzor/Yin-and-Yang-Theme",
363 | "screenshot": "assets/screenshot.png",
364 | "modes": ["dark", "light"],
365 | "legacy": true
366 | },
367 | {
368 | "name": "Golden Topaz",
369 | "author": "Mouth on Cloud",
370 | "repo": "shaggyfeng/obsidian-Golden-Topaz-theme",
371 | "screenshot": "screenshot.png",
372 | "modes": ["dark", "light"]
373 | },
374 | {
375 | "name": "Pink Topaz",
376 | "author": "Mouth on Cloud",
377 | "repo": "shaggyfeng/obsidian-Pink-topaz-theme",
378 | "screenshot": "screenshot.png",
379 | "modes": ["dark", "light"]
380 | },
381 | {
382 | "name": "Dark Moss",
383 | "author": "sergey",
384 | "repo": "sergey900553/obsidian_githublike_theme",
385 | "screenshot": "screenshot.png",
386 | "modes": ["dark"]
387 | },
388 | {
389 | "name": "Mammoth",
390 | "author": "Witt Allen",
391 | "repo": "Wittionary/mammoth-obsidian-theme",
392 | "screenshot": "screenshots/thumbnail.png",
393 | "modes": ["dark"]
394 | },
395 | {
396 | "name": "Rmaki",
397 | "author": "Luke Ruokaismaki",
398 | "repo": "luke-rmaki/rmaki-obsidian",
399 | "screenshot": "screenshot.png",
400 | "modes": ["dark"]
401 | },
402 | {
403 | "name": "Cyber Glow",
404 | "author": "ThePharaohArt",
405 | "repo": "ThePharaohArt/Obsidian-CyberGlow",
406 | "screenshot": "Screenshot.png",
407 | "modes": ["dark", "light"]
408 | },
409 | {
410 | "name": "Darkyan",
411 | "author": "johackim",
412 | "repo": "johackim/obsidian-darkyan",
413 | "screenshot": "screenshot.png",
414 | "modes": ["dark"]
415 | },
416 | {
417 | "name": "Everforest",
418 | "author": "MrGlitchByte",
419 | "repo": "mrglitchbyte/obsidian_everforest",
420 | "screenshot": "dark_v2.png",
421 | "modes": ["dark", "light"]
422 | },
423 | {
424 | "name": "Blackbird",
425 | "author": "Ivan Chernov",
426 | "repo": "vanadium23/obsidian-blackbird-theme",
427 | "screenshot": "images/example.png",
428 | "modes": ["dark"]
429 | },
430 | {
431 | "name": "Behave dark",
432 | "author": "Chrismettal",
433 | "repo": "Chrismettal/Obsidian-Behave-dark",
434 | "screenshot": "Screenshot.png",
435 | "modes": ["dark"]
436 | },
437 | {
438 | "name": "Obsidian Windows 98 Edition",
439 | "author": "SMUsamaShah",
440 | "repo": "SMUsamaShah/Obsidian-Win98-Edition",
441 | "screenshot": "screenshots/main.png",
442 | "modes": ["dark", "light"],
443 | "legacy": true
444 | },
445 | {
446 | "name": "Lizardmen Zettelkasten",
447 | "author": "lizardmenfromspace",
448 | "repo": "dogwaddle/lizardmen-zettelkasten",
449 | "screenshot": "screenshot.png",
450 | "modes": ["light"],
451 | "legacy": true
452 | },
453 | {
454 | "name": "Shimmering Focus",
455 | "author": "pseudometa",
456 | "repo": "chrisgrieser/shimmering-focus",
457 | "screenshot": "assets/promo-screenshot.webp",
458 | "modes": ["dark", "light"]
459 | },
460 | {
461 | "name": "Firefly",
462 | "author": "Ali Soueidan",
463 | "repo": "lazercaveman/obsidian-firefly-theme",
464 | "screenshot": "firefly-theme-screenshot.png",
465 | "modes": ["dark"]
466 | },
467 | {
468 | "name": "Purple Owl",
469 | "author": "zacharyc",
470 | "repo": "zacharyc/purple-owl-theme",
471 | "screenshot": "purple-owl-theme.png",
472 | "modes": ["dark"]
473 | },
474 | {
475 | "name": "Emerald",
476 | "author": "Piglet1236",
477 | "repo": "gracejoseph1236/obsidian-emerald",
478 | "screenshot": "example.png",
479 | "modes": ["dark"],
480 | "legacy": true
481 | },
482 | {
483 | "name": "Sodalite",
484 | "author": "tomzorz",
485 | "repo": "tomzorz/Sodalite",
486 | "screenshot": "screenshot.png",
487 | "modes": ["dark"]
488 | },
489 | {
490 | "name": "Faded",
491 | "author": "Josh Kasap",
492 | "repo": "JoshKasap/Obsidian-Faded-Theme",
493 | "screenshot": "Faded.png",
494 | "modes": ["dark"]
495 | },
496 | {
497 | "name": "Sanctum",
498 | "author": "jdanielmourao",
499 | "repo": "jdanielmourao/obsidian-sanctum",
500 | "screenshot": "cover.png",
501 | "modes": ["dark", "light"]
502 | },
503 | {
504 | "name": "Cardstock",
505 | "author": "cassidoo",
506 | "repo": "cassidoo/cardstock",
507 | "screenshot": "miniscreenshot.png",
508 | "modes": ["dark", "light"]
509 | },
510 | {
511 | "name": "Pine Forest Berry",
512 | "author": "Nilahn",
513 | "repo": "Nilahn/pine_forest_berry",
514 | "screenshot": "Screenshot PFB 1.png",
515 | "modes": ["dark", "light"]
516 | },
517 | {
518 | "name": "Rosé Pine Moon",
519 | "author": "mimishahzad",
520 | "repo": "mimishahzad/rose-pine-moon-obsidian",
521 | "screenshot": "assets/template.png",
522 | "modes": ["dark"]
523 | },
524 | {
525 | "name": "Ruby",
526 | "author": "Piglet1236",
527 | "repo": "gracejoseph1236/obsidian-ruby",
528 | "screenshot": "example.png",
529 | "modes": ["dark"],
530 | "legacy": true
531 | },
532 | {
533 | "name": "Prism",
534 | "author": "Damian Korcz",
535 | "repo": "damiankorcz/Prism-Theme",
536 | "screenshot": "assets/screenshots/Community Themes Screenshot.png",
537 | "modes": ["dark", "light"]
538 | },
539 | {
540 | "name": "Carnelian",
541 | "author": "Piglet1236",
542 | "repo": "gracejoseph1236/obsidian-carnelian",
543 | "screenshot": "example.png",
544 | "modes": ["dark"],
545 | "legacy": true
546 | },
547 | {
548 | "name": "Ebullientworks",
549 | "author": "Erin Schnabel",
550 | "repo": "ebullient/obsidian-theme-ebullientworks",
551 | "screenshot": "images/ebullientworks-theme.jpg",
552 | "modes": ["dark", "light"]
553 | },
554 | {
555 | "name": "Purple Aurora",
556 | "author": "