From 50fc47e6e387c3b278526ef773badf63913389d6 Mon Sep 17 00:00:00 2001 From: Terry Truong Date: Fri, 6 Jan 2023 16:59:54 +1100 Subject: Add settings modal Add saving, loading, default vals, and device-detection, to store.ts. Add setting for hiding minor tick labels. --- src/App.vue | 9 +- src/components/SButton.vue | 12 ++ src/components/SettingsModal.vue | 127 ++++++++++++++++++++ src/components/TimeLine.vue | 15 ++- src/index.css | 13 +++ src/lib.ts | 19 ++- src/store.ts | 244 ++++++++++++++++++++++++++++++++------- 7 files changed, 387 insertions(+), 52 deletions(-) create mode 100644 src/components/SButton.vue create mode 100644 src/components/SettingsModal.vue diff --git a/src/App.vue b/src/App.vue index cbd6825..70cd390 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,7 +8,7 @@ - + @@ -37,6 +37,9 @@ + + + @@ -47,6 +50,7 @@ import TimeLine from './components/TimeLine.vue'; import BaseLine from './components/BaseLine.vue'; import InfoModal from './components/InfoModal.vue'; import SearchModal from './components/SearchModal.vue'; +import SettingsModal from './components/SettingsModal.vue'; import IconButton from './components/IconButton.vue'; // Icons import HelpIcon from './components/icon/HelpIcon.vue'; @@ -314,6 +318,9 @@ function onSearch(event: HistEvent){ timelineTargets.value.splice(timelineIdx, 1, [event, !oldFlag[1]]); } +// For settings modal +const settingsOpen = ref(false); + // For resize handling let lastResizeHdlrTime = 0; // Used to throttle resize handling let afterResizeHdlr = 0; // Used to trigger handler after ending a run of resize events diff --git a/src/components/SButton.vue b/src/components/SButton.vue new file mode 100644 index 0000000..487d6bd --- /dev/null +++ b/src/components/SButton.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue new file mode 100644 index 0000000..1ccea2a --- /dev/null +++ b/src/components/SettingsModal.vue @@ -0,0 +1,127 @@ + + + diff --git a/src/components/TimeLine.vue b/src/components/TimeLine.vue index e41a729..13589d2 100644 --- a/src/components/TimeLine.vue +++ b/src/components/TimeLine.vue @@ -38,11 +38,14 @@ :style="tickStyles(tick)" class="animate-fadein"/> - - {{tick.date.toDisplayString()}} - +
@@ -1222,7 +1225,7 @@ function eventImgStyles(eventId: number){ //backgroundImage: `url(${getImagePath(event.imgId)})`, backgroundColor: 'black', backgroundSize: 'cover', - borderColor: isSearchResult ? 'red' : (event.ctg == 'discovery' ? store.color.alt2 : store.color.altDark), + borderColor: isSearchResult ? 'red' : (event.ctg == 'discovery' ? '#2563eb' : store.color.altDark), borderWidth: '1px', }; } diff --git a/src/index.css b/src/index.css index e16c538..2a7819b 100644 --- a/src/index.css +++ b/src/index.css @@ -38,6 +38,19 @@ background-color: transparent; } } +.animate-flash-yellow { + animation-name: flash-yellow; + animation-duration: 700ms; + animation-timing-function: ease-in; +} +@keyframes flash-yellow { + from { + color: #ca8a04; + } + to { + color: inherit; + } +} /* Other */ @font-face { diff --git a/src/lib.ts b/src/lib.ts index d26c8df..3b0bc68 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -5,8 +5,25 @@ import {RBTree} from './rbtree'; export const DEBUG = true; -export let WRITING_MODE_HORZ = true; +// For detecting screen size +export type Breakpoint = 'sm' | 'md' | 'lg'; +export function getBreakpoint(): Breakpoint { + const w = window.innerWidth; + if (w < 768){ + return 'sm'; + } else if (w < 1024){ + return 'md'; + } else { + return 'lg'; + } +} +// For detecting a touch device +export function onTouchDevice(){ + return window.matchMedia('(pointer: coarse)').matches; +} +// For detecting writing-mode // Used with ResizeObserver callbacks, to determine which resized dimensions are width and height +export let WRITING_MODE_HORZ = true; if ('writing-mode' in window.getComputedStyle(document.body)){ // Can be null when testing WRITING_MODE_HORZ = window.getComputedStyle(document.body)['writing-mode' as any].startsWith('horizontal'); } diff --git a/src/store.ts b/src/store.ts index 393e4b8..19a184b 100644 --- a/src/store.ts +++ b/src/store.ts @@ -3,52 +3,208 @@ */ import {defineStore} from 'pinia'; -import {CalDate} from './lib'; +import {HistDate, CalDate} from './lib'; +import {getBreakpoint, Breakpoint, onTouchDevice} from './lib'; + +export type StoreState = { + // Device info + touchDevice: boolean, + breakpoint: Breakpoint, + // Tick display + tickLen: number, //px + largeTickLen: number, + endTickSz: number // Size for start/end ticks + tickLabelHeight: number, + minTickSep: number, // Smallest gap between ticks + minLastTicks: number // When at smallest scale, don't zoom further into less than this many ticks + defaultEndTickOffset: number, // Default fraction of a unit to offset start/end ticks + showMinorTicks: boolean, + // Mainline and event display + mainlineBreadth: number, // Breadth of mainline area (incl ticks and labels) + eventImgSz: number, // Width/height of event images + eventLabelHeight: number, + spacing: number, // Spacing between display edge, events, and mainline area + // User input + scrollRatio: number, // Fraction of timeline length to move by upon scroll + zoomRatio: number, // Ratio of timeline expansion upon zooming out (eg: 1.5) + dragInertia: number, // Multiplied by final-drag-speed (pixels-per-sec) to get extra scroll distance + disableShortcuts: boolean, + // Other feature-specific + showEventCounts: boolean, + searchSuggLimit: number, + ctgs: { // Specifies event categories, and which ones should be visible + event: boolean, + place: boolean, + organism: boolean, + person: boolean, + work: boolean, + discovery: boolean, + }, + // Other + initialStartDate: HistDate, + initialEndDate: HistDate, // Must be later than initialStartDate + color: { + text: string, // CSS color + textDark: string, + bg: string, + bgLight: string, + bgDark: string, + bgLight2: string, + bgDark2: string, + alt: string, + altDark: string, + altDark2: string, + altBg: string, + bgAlt: string, + bgAltDark: string, + }, + borderRadius: number, // px + transitionDuration: number, // ms +}; +function getDefaultState(): StoreState { + const breakpoint = getBreakpoint(); + const color = { + text: '#fafaf9', // stone-50 + textDark: '#a8a29e', // stone-400 + bg: '#292524', // stone-800 + bgLight: '#44403c', // stone-700 + bgDark: '#1c1917', // stone-900 + bgLight2: '#57534e', // stone-600 + bgDark2: '#0e0c0b', // darker version of stone-900 + alt: '#fde047', // yellow-300 + altDark: '#eab308', // yellow-500 + altDark2: '#ca8a04', // yellow-600 + altBg: '#6a5e2e', + bgAlt: '#f5f5f4', // stone-100 + bgAltDark: '#d6d3d1', // stone-300 + }; + return { + // Device info + touchDevice: onTouchDevice(), + breakpoint: breakpoint, + // Tick display + tickLen: 16, + largeTickLen: 32, + endTickSz: 8, + tickLabelHeight: 10, + minTickSep: 30, + minLastTicks: 3, + defaultEndTickOffset: 0.5, + showMinorTicks: true, + // Mainline and event display + mainlineBreadth: 80, + eventImgSz: 100, + eventLabelHeight: 20, + spacing: 10, + // User input + scrollRatio: 0.2, + zoomRatio: 1.5, + dragInertia: 0.1, + disableShortcuts: false, + // Other feature-specific + showEventCounts: true, + searchSuggLimit: 10, + ctgs: { + event: true, + place: true, + organism: true, + person: true, + work: true, + discovery: true, + }, + // Other + initialStartDate: new CalDate(1900, 1, 1), + initialEndDate: new CalDate(2000, 1, 1), + color, + borderRadius: 5, + transitionDuration: 300, + }; +} + +// Gets 'composite keys' which have the form 'key1' or 'key1.key2' (usable to specify properties of store objects) +function getCompositeKeys(state: StoreState){ + const compKeys = []; + for (const key of Object.getOwnPropertyNames(state) as (keyof StoreState)[]){ + if (typeof state[key] != 'object'){ + compKeys.push(key); + } else { + for (const subkey of Object.getOwnPropertyNames(state[key])){ + compKeys.push(`${key}.${subkey}`); + } + } + } + return compKeys; +} +const STORE_COMP_KEYS = getCompositeKeys(getDefaultState()); +// For getting/setting values in store +function getStoreVal(state: StoreState, compKey: string): any { + if (compKey in state){ + return state[compKey as keyof StoreState]; + } + const [s1, s2] = compKey.split('.', 2); + if (s1 in state){ + const key1 = s1 as keyof StoreState; + if (typeof state[key1] == 'object' && s2 in (state[key1] as any)){ + return (state[key1] as any)[s2]; + } + } + return null; +} +function setStoreVal(state: StoreState, compKey: string, val: any): void { + if (compKey in state){ + (state[compKey as keyof StoreState] as any) = val; + return; + } + const [s1, s2] = compKey.split('.', 2); + if (s1 in state){ + const key1 = s1 as keyof StoreState; + if (typeof state[key1] == 'object' && s2 in (state[key1] as any)){ + (state[key1] as any)[s2] = val; + return; + } + } +} +// For loading settings into [initial] store state +function loadFromLocalStorage(state: StoreState){ + for (const key of STORE_COMP_KEYS){ + const item = localStorage.getItem(key) + if (item != null){ + setStoreVal(state, key, JSON.parse(item)); + } + } +} export const useStore = defineStore('store', { state: () => { - const color = { // Note: For scrollbar colors on chrome, edit ./index.css - text: '#fafaf9', // stone-50 - textDark: '#a8a29e', // stone-400 - bg: '#292524', // stone-800 - bgLight: '#44403c', // stone-700 - bgDark: '#1c1917', // stone-900 - bgLight2: '#57534e', // stone-600 - bgDark2: '#0e0c0b', // darker version of stone-900 - alt: '#fde047', // yellow-300 - altDark: '#eab308', // yellow-500 - altDark2: '#ca8a04', // yellow-600 - altBg: '#6a5e2e', - alt2: '#2563eb', // sky-600 - bgAlt: '#f5f5f4', // stone-100 - bgAltDark: '#d6d3d1', // stone-300 - }; - return { - tickLen: 16, - largeTickLen: 32, - endTickSz: 8, // Size for start/end ticks - tickLabelHeight: 10, - minTickSep: 30, // Smallest px separation between ticks - minLastTicks: 3, // When at smallest scale, don't zoom further into less than this many ticks - defaultEndTickOffset: 0.5, // Default fraction of a unit to offset start/end ticks - // - mainlineBreadth: 80, // Breadth of mainline area (including ticks and labels) - eventImgSz: 100, // Width/height of event images - eventLabelHeight: 20, - spacing: 10, // Spacing between display edge, events, and mainline area - // - scrollRatio: 0.2, // Fraction of timeline length to move by upon scroll - zoomRatio: 1.5, // Ratio of timeline expansion upon zooming out - dragInertia: 0.1, // Multiplied by final-drag-speed (pixels-per-sec) to get extra scroll distance - // - initialStartDate: new CalDate(1900, 1, 1), - initialEndDate: new CalDate(2000, 1, 1), - color, - showEventCounts: true, - transitionDuration: 300, - borderRadius: 5, // px - searchSuggLimit: 10, - disableShortcuts: false, - }; + const state = getDefaultState(); + loadFromLocalStorage(state); + return state; + }, + actions: { + reset(): void { + Object.assign(this, getDefaultState()); + }, + resetOne(key: string){ + const val = getStoreVal(this, key); + if (val != null){ + const val2 = getStoreVal(getDefaultState(), key); + if (val != val2){ + setStoreVal(this, key, val2); + } + } + }, + save(key: string){ + if (STORE_COMP_KEYS.includes(key)){ + localStorage.setItem(key, JSON.stringify(getStoreVal(this, key))); + } + }, + load(): void { + loadFromLocalStorage(this); + }, + clear(): void { + for (const key of STORE_COMP_KEYS){ + localStorage.removeItem(key); + } + }, }, }); -- cgit v1.2.3