Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions web_src/js/components/DiffFileTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import {toggleElem} from '../utils/dom.ts';
import {diffTreeStore} from '../modules/diff-file.ts';
import {setFileFolding} from '../features/file-fold.ts';
import {onMounted, onUnmounted} from 'vue';
import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts';

const LOCAL_STORAGE_KEY = 'diff_file_tree_visible';

const store = diffTreeStore();

onMounted(() => {
// Default to true if unset
store.fileTreeIsVisible = localStorage.getItem(LOCAL_STORAGE_KEY) !== 'false';
store.fileTreeIsVisible = getLocalStorageSetting(LOCAL_STORAGE_KEY) !== 'false';
document.querySelector('.diff-toggle-file-tree-button')!.addEventListener('click', toggleVisibility);

hashChangeListener();
Expand Down Expand Up @@ -43,7 +44,7 @@ function toggleVisibility() {

function updateVisibility(visible: boolean) {
store.fileTreeIsVisible = visible;
localStorage.setItem(LOCAL_STORAGE_KEY, store.fileTreeIsVisible.toString());
setLocalStorageSetting(LOCAL_STORAGE_KEY, store.fileTreeIsVisible.toString());
updateState(store.fileTreeIsVisible);
}

Expand Down
5 changes: 3 additions & 2 deletions web_src/js/components/RepoActionView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {renderAnsi} from '../render/ansi.ts';
import {POST, DELETE} from '../modules/fetch.ts';
import type {IntervalId} from '../types.ts';
import {toggleFullScreen} from '../utils.ts';
import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts';

// see "models/actions/status.go", if it needs to be used somewhere else, move it to a shared file like "types/actions.ts"
type RunStatus = 'unknown' | 'waiting' | 'running' | 'success' | 'failure' | 'cancelled' | 'skipped' | 'blocked';
Expand Down Expand Up @@ -73,7 +74,7 @@ type LocaleStorageOptions = {

function getLocaleStorageOptions(): LocaleStorageOptions {
try {
const optsJson = localStorage.getItem('actions-view-options');
const optsJson = getLocalStorageSetting('actions-view-options');
if (optsJson) return JSON.parse(optsJson);
} catch {}
// if no options in localStorage, or failed to parse, return default options
Expand Down Expand Up @@ -224,7 +225,7 @@ export default defineComponent({
methods: {
saveLocaleStorageOptions() {
const opts: LocaleStorageOptions = {autoScroll: this.optionAlwaysAutoScroll, expandRunning: this.optionAlwaysExpandRunning};
localStorage.setItem('actions-view-options', JSON.stringify(opts));
setLocalStorageSetting('actions-view-options', JSON.stringify(opts));
},

// get the job step logs container ('.job-step-logs')
Expand Down
7 changes: 4 additions & 3 deletions web_src/js/features/citation.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {getCurrentLocale} from '../utils.ts';
import {fomanticQuery} from '../modules/fomantic/base.ts';
import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts';

const {pageData} = window.config;

Expand Down Expand Up @@ -38,7 +39,7 @@ export async function initCitationFileCopyContent() {
if ((!citationCopyApa && !citationCopyBibtex) || !inputContent) return;

const updateUi = () => {
const isBibtex = (localStorage.getItem('citation-copy-format') || defaultCitationFormat) === 'bibtex';
const isBibtex = (getLocalStorageSetting('citation-copy-format') || defaultCitationFormat) === 'bibtex';
const copyContent = (isBibtex ? citationCopyBibtex : citationCopyApa).getAttribute('data-text')!;
inputContent.value = copyContent;
citationCopyBibtex.classList.toggle('primary', isBibtex);
Expand All @@ -55,12 +56,12 @@ export async function initCitationFileCopyContent() {
updateUi();

citationCopyApa.addEventListener('click', () => {
localStorage.setItem('citation-copy-format', 'apa');
setLocalStorageSetting('citation-copy-format', 'apa');
updateUi();
});

citationCopyBibtex.addEventListener('click', () => {
localStorage.setItem('citation-copy-format', 'bibtex');
setLocalStorageSetting('citation-copy-format', 'bibtex');
updateUi();
});

Expand Down
33 changes: 27 additions & 6 deletions web_src/js/features/comp/ComboMarkdownEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {DropzoneCustomEventReloadFiles, initDropzone} from '../dropzone.ts';
import {createTippy} from '../../modules/tippy.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
import type EasyMDE from 'easymde';
import {addLocalStorageChangeListener, getLocalStorageSetting, setLocalStorageSetting} from '../../modules/storage.ts';

/**
* validate if the given textarea is non-empty.
Expand All @@ -47,6 +48,21 @@ export function validateTextareaNonEmpty(textarea: HTMLTextAreaElement) {
return true;
}

/** Returns whether the user currently has the monospace font setting enabled */
function isMonospaceEnabled() {
return getLocalStorageSetting('markdown-editor-monospace') === 'true';
}

/** Apply font to the provided or all textareas on the page and optionally save on localStorage */
function applyMonospace(enabled: boolean, {textarea, save}: {textarea?: HTMLTextAreaElement, save?: boolean}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should be a method of ComboMarkdownEditor

And to change others, getComboMarkdownEditor and call their instance's method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be possible if there's a way to get references to all current ComboMarkdownEditor on the page.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use getComboMarkdownEditor for the related elements to get all current ComboMarkdownEditor instances

for (const el of textarea ? [textarea] : document.querySelectorAll('.markdown-text-editor')) {
el.classList.toggle('tw-font-mono', enabled);
}
if (save) {
setLocalStorageSetting('markdown-editor-monospace', String(enabled));
}
}

type Heights = {
minHeight?: string,
height?: string,
Expand Down Expand Up @@ -141,20 +157,25 @@ export class ComboMarkdownEditor {
}

const monospaceButton = this.container.querySelector('.markdown-switch-monospace')!;
const monospaceEnabled = localStorage?.getItem('markdown-editor-monospace') === 'true';
const monospaceEnabled = isMonospaceEnabled();
applyMonospace(monospaceEnabled, {textarea: this.textarea, save: false});
const monospaceText = monospaceButton.getAttribute(monospaceEnabled ? 'data-disable-text' : 'data-enable-text')!;
monospaceButton.setAttribute('data-tooltip-content', monospaceText);
monospaceButton.setAttribute('aria-checked', String(monospaceEnabled));
monospaceButton.addEventListener('click', (e) => {
e.preventDefault();
const enabled = localStorage?.getItem('markdown-editor-monospace') !== 'true';
localStorage.setItem('markdown-editor-monospace', String(enabled));
this.textarea.classList.toggle('tw-font-mono', enabled);
const enabled = !isMonospaceEnabled();
applyMonospace(enabled, {save: true});
const text = monospaceButton.getAttribute(enabled ? 'data-disable-text' : 'data-enable-text')!;
monospaceButton.setAttribute('data-tooltip-content', text);
monospaceButton.setAttribute('aria-checked', String(enabled));
});

// apply setting when it was changed in another tab
addLocalStorageChangeListener('markdown-editor-monospace', () => {
applyMonospace(isMonospaceEnabled(), {save: false});
});

if (this.supportEasyMDE) {
const easymdeButton = this.container.querySelector('.markdown-switch-easymde')!;
easymdeButton.addEventListener('click', async (e) => {
Expand Down Expand Up @@ -403,10 +424,10 @@ export class ComboMarkdownEditor {
}

get userPreferredEditor(): string {
return window.localStorage.getItem(`markdown-editor-${this.previewMode ?? 'default'}`) || '';
return getLocalStorageSetting(`markdown-editor-${this.previewMode ?? 'default'}`) || '';
}
set userPreferredEditor(s: string) {
window.localStorage.setItem(`markdown-editor-${this.previewMode ?? 'default'}`, s);
setLocalStorageSetting(`markdown-editor-${this.previewMode ?? 'default'}`, s);
}
}

Expand Down
9 changes: 5 additions & 4 deletions web_src/js/features/repo-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import RepoActivityTopAuthors from '../components/RepoActivityTopAuthors.vue';
import {createApp} from 'vue';
import {toOriginUrl} from '../utils/url.ts';
import {createTippy} from '../modules/tippy.ts';
import {getLocalStorageSetting, setLocalStorageSetting} from '../modules/storage.ts';

async function onDownloadArchive(e: Event) {
e.preventDefault();
Expand Down Expand Up @@ -57,7 +58,7 @@ function initCloneSchemeUrlSelection(parent: Element) {
const tabSsh = parent.querySelector('.repo-clone-ssh');
const tabTea = parent.querySelector('.repo-clone-tea');
const updateClonePanelUi = function() {
let scheme = localStorage.getItem('repo-clone-protocol')!;
let scheme = getLocalStorageSetting('repo-clone-protocol')!;
if (!['https', 'ssh', 'tea'].includes(scheme)) {
scheme = 'https';
}
Expand Down Expand Up @@ -114,15 +115,15 @@ function initCloneSchemeUrlSelection(parent: Element) {
updateClonePanelUi();
// tabSsh or tabHttps might not both exist, eg: guest view, or one is disabled by the server
tabHttps?.addEventListener('click', () => {
localStorage.setItem('repo-clone-protocol', 'https');
setLocalStorageSetting('repo-clone-protocol', 'https');
updateClonePanelUi();
});
tabSsh?.addEventListener('click', () => {
localStorage.setItem('repo-clone-protocol', 'ssh');
setLocalStorageSetting('repo-clone-protocol', 'ssh');
updateClonePanelUi();
});
tabTea?.addEventListener('click', () => {
localStorage.setItem('repo-clone-protocol', 'tea');
setLocalStorageSetting('repo-clone-protocol', 'tea');
updateClonePanelUi();
});
elCloneUrlInput.addEventListener('focus', () => {
Expand Down
18 changes: 18 additions & 0 deletions web_src/js/modules/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/** Get a setting from localStorage */
export function getLocalStorageSetting(key: string) {
return localStorage?.getItem(key);
}

/** Set a setting in localStorage */
export function setLocalStorageSetting(key: string, value: string) {
return localStorage?.setItem(key, value);
}
Comment on lines +1 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do these two wrapper functions help?

Why not just write localStorage.getItem()?

Copy link
Member Author

@silverwind silverwind Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The wrappers are in preparation to synchronize these settings with the server, e.g. the current settings would be rendered into <head> and it would trigger a POST when a setting changes. For non-logged-in users, localStorage will be continued to be used. So to support both cases, these wrappers are needed.

Maybe the functions should have a better name like getUserSetting.

Copy link
Member Author

@silverwind silverwind Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought this addLocalStorageChangeListener could not be implemented with server-backed storage without a websocket, I suppose. Maybe I should remove it.


/** Add a listener to the 'storage' event for given setting key. This event only fires in non-current tabs. */
export function addLocalStorageChangeListener(key: string, listener: (e: StorageEvent) => void) {
window.addEventListener('storage', (e: StorageEvent) => {
if (e.storageArea === localStorage && e.key === key) {
listener(e);
}
});
}