diff --git a/app/components/ControlAspectRatios.vue b/app/components/ControlAspectRatios.vue index c1c2490..6f52fdc 100644 --- a/app/components/ControlAspectRatios.vue +++ b/app/components/ControlAspectRatios.vue @@ -1,5 +1,6 @@ + + + +
+ + : + + +
+
+ + + + + + + + No saved ratios + + + + + + +
@@ -432,15 +505,18 @@ diff --git a/app/composables/useAspectRatios.js b/app/composables/useAspectRatios.js index f3b4af0..f02966b 100644 --- a/app/composables/useAspectRatios.js +++ b/app/composables/useAspectRatios.js @@ -1,9 +1,32 @@ +import { computed } from 'vue'; +import usePreferencesStore from '@/composables/usePreferencesStore'; + +function validAspectRatio(ratio) { + return ( + Array.isArray(ratio) && + ratio.length === 2 && + ratio.every((value) => Number.isFinite(Number(value)) && Number(value) > 0) + ); +} + +function normalizeAspectRatio([x, y]) { + return [Number(x), Number(y)]; +} + export default function () { - const aspectRatios = [ - [16, 9], - [4, 3], - [1, 1], - ]; + const preferences = usePreferencesStore(); + + const aspectRatios = computed(() => { + const ratios = preferences.previewAspectRatios + .filter(validAspectRatio) + .map(normalizeAspectRatio); + + return ratios.filter((ratio, index, collection) => { + const [x, y] = ratio; + + return collection.findIndex(([rx, ry]) => rx === x && ry === y) === index; + }); + }); function calculateAspectRatio([x, y], height) { return Math.round((height / y) * x); diff --git a/app/composables/usePreferencesStore.js b/app/composables/usePreferencesStore.js index 07de956..c96c1d8 100644 --- a/app/composables/usePreferencesStore.js +++ b/app/composables/usePreferencesStore.js @@ -3,6 +3,12 @@ import { useLocalStorage } from '@vueuse/core'; import themes from 'monaco-themes/themes/themelist.json'; import { pick, defaults as applyDefaults } from 'lodash'; +export const defaultAspectRatios = [ + [16, 9], + [4, 3], + [1, 1], +]; + export const defaults = { editorTabSize: 4, editorFontSize: 12, @@ -20,6 +26,7 @@ export const defaults = { previewCodeBlurStrength: 1, previewFontFamily: 'font-mono-lisa', previewThemeName: 'github-dark', + previewAspectRatios: defaultAspectRatios, previewLockToWindow: false, previewLockToWindowPaddingX: 0, @@ -36,10 +43,21 @@ export const defaults = { socialPosition: 'bottom-center', }; +function sameAspectRatio([leftX, leftY], [rightX, rightY]) { + return Number(leftX) === Number(rightX) && Number(leftY) === Number(rightY); +} + export default defineStore('preferences', { state: () => { const state = useLocalStorage('preferences', defaults); + if (!state.value.previewAspectRatios && state.value.previewCustomAspectRatios) { + state.value.previewAspectRatios = [ + ...defaultAspectRatios, + ...state.value.previewCustomAspectRatios, + ]; + } + // Here we are enforcing the hydration of the default // preference values and also removing any keys // that may have been removed from an update. @@ -69,5 +87,23 @@ export default defineStore('preferences', { this.$state = defaults; } }, + + resetAspectRatios() { + this.previewAspectRatios = defaultAspectRatios.map((ratio) => [...ratio]); + }, + + hasAspectRatio(ratio) { + return this.previewAspectRatios.some((existingRatio) => + sameAspectRatio(existingRatio, ratio) + ); + }, + + addAspectRatio(ratio) { + if (this.hasAspectRatio(ratio)) { + return; + } + + this.previewAspectRatios.push(ratio); + }, }, }); diff --git a/app/content/changelog.md b/app/content/changelog.md index f619c05..5a46fd2 100644 --- a/app/content/changelog.md +++ b/app/content/changelog.md @@ -1,3 +1,16 @@ +## June 22, 2026 — Version 2.7 + +**Added** + +- Added configurable aspect ratios in Preferences +- Added the ability to add, remove, and reset preview aspect ratios + +**Changed** + +- The aspect ratio bar now reflects the configured preference list and hides when no ratios are defined + +--- + ## June 19, 2026 — Version 2.6 **Added** diff --git a/package-lock.json b/package-lock.json index 3ba3f52..62ed966 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "showcode", - "version": "2.6.0", + "version": "2.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "showcode", - "version": "2.6.0", + "version": "2.7.0", "license": "MIT", "devDependencies": { "@formkit/auto-animate": "^0.9.0", diff --git a/package.json b/package.json index 2962681..86eebde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "showcode", - "version": "2.6.0", + "version": "2.7.0", "private": true, "description": "Generate beautiful images of code.", "repository": { diff --git a/tests/components/ControlAspectRatios.test.js b/tests/components/ControlAspectRatios.test.js new file mode 100644 index 0000000..edcf3bd --- /dev/null +++ b/tests/components/ControlAspectRatios.test.js @@ -0,0 +1,31 @@ +import { mount } from '@vue/test-utils'; +import { describe, expect, it } from 'vitest'; +import ControlAspectRatios from '~/components/ControlAspectRatios.vue'; + +describe('ControlAspectRatios', () => { + it('renders available aspect ratios and custom option', () => { + const wrapper = mount(ControlAspectRatios, { + props: { + aspectRatio: [16, 9], + aspectRatios: [[16, 9]], + lockWindowSize: false, + }, + }); + + expect(wrapper.text()).toContain('16:9'); + expect(wrapper.text()).toContain('Custom'); + }); + + it('does not render when no aspect ratios are available', () => { + const wrapper = mount(ControlAspectRatios, { + props: { + aspectRatio: null, + aspectRatios: [], + lockWindowSize: false, + }, + }); + + expect(wrapper.find('button').exists()).toBe(false); + expect(wrapper.text()).toBe(''); + }); +}); diff --git a/tests/composables/useAspectRatios.test.js b/tests/composables/useAspectRatios.test.js index e60ca25..1148bbe 100644 --- a/tests/composables/useAspectRatios.test.js +++ b/tests/composables/useAspectRatios.test.js @@ -1,11 +1,85 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeEach } from 'vitest'; import useAspectRatios from '~/composables/useAspectRatios'; +import usePreferencesStore from '~/composables/usePreferencesStore'; describe('useAspectRatios', () => { + beforeEach(() => { + localStorage.clear(); + }); + it('returns the default aspect ratios', () => { const { aspectRatios } = useAspectRatios(); - expect(aspectRatios).toEqual([ + expect(aspectRatios.value).toEqual([ + [16, 9], + [4, 3], + [1, 1], + ]); + }); + + it('returns aspect ratios from preferences', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [ + [21, 9], + [5, 4], + ]; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([ + [21, 9], + [5, 4], + ]); + }); + + it('can return no aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = []; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([]); + }); + + it('ignores duplicate aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [ + [16, 9], + [21, 9], + [21, 9], + ]; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([ + [16, 9], + [21, 9], + ]); + }); + + it('ignores invalid aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [ + [21, 9], + [0, 9], + [-1, 9], + [4], + ['wide', 9], + null, + ]; + + const { aspectRatios } = useAspectRatios(); + + expect(aspectRatios.value).toEqual([[21, 9]]); + }); + + it('resets aspect ratios to defaults', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [[21, 9]]; + + preferences.resetAspectRatios(); + + expect(preferences.previewAspectRatios).toEqual([ [16, 9], [4, 3], [1, 1], @@ -32,4 +106,3 @@ describe('useAspectRatios', () => { expect(calculateAspectRatio([1, 1], 500)).toBe(500); }); }); - diff --git a/tests/composables/usePreferencesStore.test.js b/tests/composables/usePreferencesStore.test.js new file mode 100644 index 0000000..bb2ff8a --- /dev/null +++ b/tests/composables/usePreferencesStore.test.js @@ -0,0 +1,27 @@ +import { describe, it, expect, beforeEach } from 'vitest'; +import usePreferencesStore from '~/composables/usePreferencesStore'; + +describe('usePreferencesStore', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('adds aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = []; + + preferences.addAspectRatio([21, 9]); + + expect(preferences.previewAspectRatios).toEqual([[21, 9]]); + }); + + it('does not add duplicate aspect ratios', () => { + const preferences = usePreferencesStore(); + preferences.previewAspectRatios = [[16, 9]]; + + preferences.addAspectRatio([16, 9]); + preferences.addAspectRatio(['16', '9']); + + expect(preferences.previewAspectRatios).toEqual([[16, 9]]); + }); +});