\ No newline at end of file
diff --git a/src/lib/context/index.svelte.ts b/src/lib/context/index.svelte.ts
new file mode 100644
index 0000000..3e1b88c
--- /dev/null
+++ b/src/lib/context/index.svelte.ts
@@ -0,0 +1,49 @@
+import { db, DEFAULT_SESSION_DATA } from "$lib/db/index.svelte";
+import type { SessionContext } from "$lib/types";
+import { getContext, hasContext, setContext } from "svelte";
+
+const CONTEXT_KEY = "session";
+
+// A wrapper class so Svelte is willing to make barTopic $state real.
+class SessionImpl implements SessionContext {
+ delegates = db.enabledDelegatesStore();
+ motions = db.sessionDataStore("motions", DEFAULT_SESSION_DATA.motions);
+ selectedMotion = db.sessionDataStore("selectedMotion", DEFAULT_SESSION_DATA.selectedMotion);
+ selectedMotionState = db.sessionDataStore("selectedMotionState", DEFAULT_SESSION_DATA.selectedMotionState);
+ speakersList = db.sessionDataStore("speakersList", DEFAULT_SESSION_DATA.speakersList);
+ barTitle = db.settingStore("title", "");
+ barTopic = $state();
+}
+
+/**
+ * Gets the session context object.
+ *
+ * This is an object which holds all properties of the session,
+ * including those accessed via the session database.
+ *
+ * This context is useful in that it allows for data to persist between page changes.
+ *
+ * @returns the context object
+ */
+export function getSessionContext() {
+ return getContext(CONTEXT_KEY);
+}
+
+/**
+ * Creates the session context object (if it hasn't been created).
+ *
+ * @returns the context object
+ */
+export function createSessionContext() {
+ if (hasContext(CONTEXT_KEY)) return getSessionContext();
+ return setContext(CONTEXT_KEY, new SessionImpl());
+}
+/**
+ * Resets properties of the session.
+ */
+export async function resetSessionContext(ctx: SessionContext) {
+ await db.resetSessionData();
+
+ // Additional attributes to clear:
+ ctx.barTopic = undefined;
+}
\ No newline at end of file
diff --git a/src/lib/db/delegates.ts b/src/lib/db/delegates.ts
new file mode 100644
index 0000000..ce9033f
--- /dev/null
+++ b/src/lib/db/delegates.ts
@@ -0,0 +1,70 @@
+/**
+ * Delegate table definition for the session database.
+ */
+
+import type { DelegateAttrs, DelegateID, DelegatePresence, DelSessionData, StatsData } from "$lib/types";
+import { Entity } from "dexie";
+import type { SessionDatabase } from "./index.svelte";
+
+export class Delegate extends Entity {
+ // Indexes:
+ id!: DelegateID;
+ name!: string;
+ aliases!: string[];
+ order!: number;
+
+ // Non-indexes:
+ enabled!: boolean;
+ flagURL!: string;
+ presence!: DelegatePresence;
+ stats!: StatsData;
+
+ static readonly indexes = "++id, name, *aliases, order";
+
+ /**
+ * Checks a delegate presence status is present.
+ * @param p the presence status
+ * @returns whether it indicates presence
+ */
+ isPresent(): boolean {
+ return this.presence != "NP";
+ }
+ /**
+ * Checks if name is associated with a given delegate.
+ * @param name name we're looking at
+ * @returns whether this delegate could correctly be referred to by the given name
+ */
+ nameEquals(name: string): boolean {
+ // case-insensitive equals
+ const eq = (a: string, b: string) => a.localeCompare(b, undefined, { sensitivity: "base" }) == 0;
+ return eq(this.name, name) || this.aliases.some(n => eq(n, name));
+ }
+
+ /**
+ * @returns the attributes of this delegate
+ */
+ getAttributes(): DelegateAttrs {
+ return {
+ name: this.name,
+ aliases: structuredClone(this.aliases),
+ flagURL: this.flagURL,
+ };
+ }
+ getSessionData(): DelSessionData {
+ return {
+ presence: this.presence,
+ stats: structuredClone(this.stats),
+ }
+ }
+}
+
+/**
+ * Find the delegate with the matching ID (note this is linear search).
+ * @param d Delegate list
+ * @param searchId Search ID
+ * @returns The matching delegate (if they exist)
+ */
+export function findDelegate(d: Delegate[], searchId: DelegateID): Delegate | undefined {
+ // linear but bleh it's synchronous so whatever
+ return d.find(({id}) => id == searchId);
+}
diff --git a/src/lib/db/index.svelte.ts b/src/lib/db/index.svelte.ts
new file mode 100644
index 0000000..0f1f4c6
--- /dev/null
+++ b/src/lib/db/index.svelte.ts
@@ -0,0 +1,302 @@
+import { Delegate } from "./delegates";
+import { KeyValuePair, toKeyValueArray, toObject } from "./keyval";
+import { DEFAULT_DELEGATES } from "$lib/delegate_presets";
+import { getFlagUrl } from "$lib/flags/flagcdn";
+import { DEFAULT_SORT_PRIORITY } from "$lib/motions/definitions";
+import type { DelegateAttrs, DelegateID, DelSessionData, PrevSessionData, SessionData, Settings } from "$lib/types";
+import { Dexie, liveQuery, type EntityTable, type IndexableType, type InsertType } from "dexie";
+import { readable, type Readable, type Updater, type Writable } from "svelte/store";
+
+export class SessionDatabase extends Dexie {
+ delegates!: EntityTable;
+ settings!: EntityTable;
+ sessionData!: EntityTable;
+ prevSessions!: EntityTable, "key">;
+
+ constructor() {
+ super("sessionDatabase", { cache: "immutable" });
+ this.version(1).stores({
+ delegates: Delegate.indexes,
+ settings: KeyValuePair.indexes,
+ sessionData: KeyValuePair.indexes,
+ prevSessions: KeyValuePair.indexes,
+ });
+ this.delegates.mapToClass(Delegate);
+ this.settings.mapToClass(KeyValuePair);
+ this.sessionData.mapToClass(KeyValuePair);
+ this.prevSessions.mapToClass(KeyValuePair);
+ }
+
+ /**
+ * Updates one entry from the delegate table.
+ *
+ * This should only be used for single-time, short updates.
+ * Large changes (such as changing multiple delegates at once)
+ * should go through Dexie's bulk update methods
+ * and multiple operations should go through transactions.
+ *
+ * @param id the ID of the entry to update
+ * @param param parameters to `Dexie.Table.update`
+ * (either a callback that updates an item or an object indicating what parameters to update)
+ * @returns a promise on completion
+ */
+ async updateDelegate(id: DelegateID | undefined, param: Parameters[1]): Promise;
+ async updateDelegate(id: DelegateID | undefined, param: ((obj: Delegate, ctx: { value: any; primKey: IndexableType; }) => void | boolean)): Promise;
+ async updateDelegate(id: DelegateID | undefined, param: any): Promise {
+ if (typeof id !== "number") return;
+ await this.delegates.update(id, param);
+ }
+
+ /**
+ * Adds a delegate with the given attributes to the delegate database.
+ * @param attrs The attributes of the new delegate
+ */
+ async addDelegate(attrs: DelegateAttrs) {
+ await this.transaction("rw", this.delegates, async () => {
+ const order = await this.delegates.count();
+ const newDel = populateDelegate(attrs, order);
+ await this.delegates.add(newDel);
+ })
+ }
+ /**
+ * Adds a set of delegates with the given attributes to the delegate database.
+ * @param delegates the attributes of the new delegates
+ */
+ async addDelegates(delegates: DelegateAttrs[]) {
+ await this.transaction("rw", this.delegates, async () => {
+ const order = await this.delegates.count();
+ const newDels = delegates.map((attrs, i) => populateDelegate(attrs, order + i));
+ await this.delegates.bulkAdd(newDels);
+ })
+ }
+
+ /**
+ * Gets a readable store of all enabled delegates.
+ * This can be used during a session to get a list of delegates.
+ *
+ * Note that this does not give a list of *only* present delegates.
+ * To do that, you need to additionally filter the result of this store:
+ *
+ * ```ts
+ * const delegates = db.enabledDelegatesStore();
+ * const presentDelegates = $delegates.filter(d => d.isPresent());
+ * ```
+ *
+ * @returns the store
+ */
+ enabledDelegatesStore(): Readable {
+ return queryStore(() => {
+ return db.delegates.orderBy("order")
+ .filter(e => e.enabled)
+ .toArray();
+ }, []);
+ }
+ /**
+ * Gets the setting from the settings database.
+ * @param key the key to get the setting for
+ * @returns the value of the setting
+ */
+ async getSetting(key: K): Promise {
+ return this.settings.get(key).then(e => e!.val);
+ }
+ /**
+ * Creates a writable store for a given setting.
+ * @param key the key to make a store for
+ * @returns the store
+ */
+ settingStore(key: K): Writable;
+ settingStore(key: K, fallback: Settings[K]): Writable;
+ settingStore(key: K, fallback?: Settings[K]): Writable {
+ return getKVStore(this.settings, key, fallback);
+ }
+ async resetSettings() {
+ await this.transaction("rw", this.settings, async () => {
+ await this.settings.clear();
+ await this.settings.bulkAdd(toKeyValueArray(DEFAULT_SETTINGS));
+ });
+ }
+
+ /**
+ * Creates a writable store for a given session data key.
+ * @param key the key to make a store for
+ * @returns the store
+ */
+ sessionDataStore(key: K): Writable;
+ sessionDataStore(key: K, fallback: SessionData[K]): Writable;
+ sessionDataStore(key: K, fallback?: SessionData[K]): Writable {
+ return getKVStore(this.sessionData, key, fallback);
+ }
+
+ async getSessionValue(key: K): Promise {
+ return this.sessionData.get(key).then(e => e?.val);
+ }
+ async resetSessionData() {
+ await this.transaction("rw", [this.sessionData, this.delegates], async () => {
+ await this.delegates.toCollection().modify(DEFAULT_DEL_SESSION_DATA);
+
+ await this.sessionData.clear();
+ await this.sessionData.bulkAdd(toKeyValueArray(DEFAULT_SESSION_DATA));
+ })
+ }
+ async saveSessionData() {
+ return this.transaction("rw", [this.delegates, this.sessionData, this.prevSessions], async () => {
+ let sessionKey = await this.getSessionValue("sessionKey") ?? await this.prevSessions.count();
+
+ let sessionData = await this.sessionData.toArray();
+ let delegates = await this.delegates.toArray();
+
+ await this.prevSessions.put({ key: sessionKey, val: {
+ common: Object.assign(toObject(sessionData) as SessionData, { sessionKey }),
+ delegates: SessionDatabase.delegatesAsSessionData(delegates)
+ } });
+ });
+ }
+ async loadSessionData(key: number) {
+ return this.transaction("rw", [this.delegates, this.sessionData, this.prevSessions], async () => {
+ let entry = await this.prevSessions.get(key);
+ if (entry) {
+ let { common, delegates } = entry.val;
+ await this.saveSessionData();
+
+ await this.delegates.bulkUpdate(
+ delegates.map(({ id, session }) => ({ key: id, changes: session }))
+ );
+
+ await this.sessionData.clear();
+ await this.sessionData.bulkAdd(toKeyValueArray(common));
+ }
+ });
+ }
+
+ /**
+ * Comverts an array of delegates into the delegate session data array
+ * @param delegates delegate
+ * @returns the new array
+ */
+ static delegatesAsSessionData(delegates: Delegate[]): PrevSessionData["delegates"] {
+ return delegates.map(d => ({ id: d.id, session: d.getSessionData() }));
+ }
+}
+
+export const db = new SessionDatabase();
+
+db.on("ready", async (tx) => {
+ let txdb = tx as typeof db;
+ if (await txdb.delegates.count() == 0) {
+ const dels = await _legacyFixDelFlag(DEFAULT_DELEGATES);
+ await txdb.addDelegates(dels);
+ }
+ if (await txdb.settings.count() == 0) {
+ await txdb.resetSettings();
+ }
+ if (await txdb.sessionData.count() == 0) {
+ await txdb.sessionData.bulkAdd(toKeyValueArray(DEFAULT_SESSION_DATA));
+ }
+})
+
+/**
+ * Default session data per delegate.
+ */
+export const DEFAULT_DEL_SESSION_DATA = {
+ presence: "NP",
+ stats: {
+ motionsProposed: 0,
+ motionsAccepted: 0,
+ timesSpoken: 0,
+ durationSpoken: 0
+ }
+} as const satisfies DelSessionData;
+
+export const DEFAULT_SESSION_DATA = {
+ motions: [],
+ selectedMotion: null,
+ selectedMotionState: {
+ speakersList: []
+ },
+ speakersList: []
+} as const satisfies SessionData;
+
+/**
+ * Default settings.
+ */
+export const DEFAULT_SETTINGS = {
+ sortOrder: DEFAULT_SORT_PRIORITY,
+ title: "General Assembly",
+ preferences: {
+ enableMotionRoundRobin: true,
+ enableMotionExt: true,
+ pauseMainTimer: true,
+ }
+} satisfies Settings;
+
+/**
+ * Adds attributes to a given delegate, allowing it to be inserted into the database.
+ * @param attrs the attributes the delegate already has
+ * @param order its position in the database
+ * @returns the new object which contains all database attributes for the delegate
+ */
+function populateDelegate(attrs: DelegateAttrs, order: number): InsertType {
+ let { name, aliases, flagURL: mFlagURL } = attrs;
+ return Object.assign({
+ name, aliases, order, enabled: true, flagURL: mFlagURL ?? ""
+ }, DEFAULT_DEL_SESSION_DATA);
+}
+/**
+ * Method to handle legacy `Record` format.
+ * @param flagKey the flag key
+ * @param attrs the delegate attributes
+ */
+export async function _legacyFixDelFlag(flagKey: string, attrs: DelegateAttrs): Promise;
+/**
+ * Method to handle legacy `Record` format.
+ * @param delegates the list of delegates, as a flag key-delegate attribute mapping.
+ */
+export async function _legacyFixDelFlag(delegates: Record): Promise;
+export async function _legacyFixDelFlag(flagKeyOrDelegates: string | Record, attrs?: DelegateAttrs): Promise {
+ if (typeof flagKeyOrDelegates === "string" && typeof attrs === "object") {
+ let flagKey = flagKeyOrDelegates;
+ let { name, aliases, flagURL: mFlagURL } = attrs;
+ let flagURL = mFlagURL ?? (await getFlagUrl(flagKey))?.href ?? (await getFlagUrl("un"))!.href;
+ return { name, aliases, flagURL };
+ } else if (typeof flagKeyOrDelegates === "object") {
+ let delegates = flagKeyOrDelegates;
+ return Promise.all(
+ Object.entries(delegates)
+ .map(([k, attrs]) => _legacyFixDelFlag(k, attrs))
+ );
+ } else {
+ throw new TypeError("Invalid arguments");
+ }
+}
+
+/**
+ * A store on a database query that updates when the database updates.
+ *
+ * This is similar to Dexie's `liveQuery`, except with some adjustments to better support Svelte.
+ * Notably, this adds the option to give a default value and doesn't reload on page switch
+ * if put in a context.
+ *
+ * @param cb the querier function
+ */
+export function queryStore(cb: () => T | Promise): Readable;
+export function queryStore(cb: () => T | Promise, fallback: T): Readable;
+export function queryStore(cb: () => T | Promise, fallback?: T) {
+ const query = liveQuery(cb);
+ return readable(fallback, (set) => query.subscribe(set).unsubscribe);
+}
+/**
+ * Creates a writable store out of a single key-value pair in a database table.
+ *
+ * This store assumes the key exists in the table. It will not write if it does not exist.
+ *
+ * @param table the table
+ * @param key the key
+ * @param fallback a fallback/default value to use before the value is first read successfully
+ */
+function getKVStore(table: EntityTable, key: string, fallback?: any): Writable {
+ const store = queryStore(() => table.get(key).then(entry => entry?.val), fallback);
+ const set = (val: any) => table.update(key, { val: $state.snapshot(val) });
+ const update = (updater: Updater) => table.update(key, entry => entry.val = updater(entry.val));
+
+ return Object.assign(store, { set, update });
+}
\ No newline at end of file
diff --git a/src/lib/db/keyval.ts b/src/lib/db/keyval.ts
new file mode 100644
index 0000000..c4ecd1a
--- /dev/null
+++ b/src/lib/db/keyval.ts
@@ -0,0 +1,24 @@
+/**
+ * Table definition of key-val stores for the session database.
+ */
+
+import { Entity } from "dexie";
+import type { SessionDatabase } from "./index.svelte";
+
+export class KeyValuePair extends Entity implements IKeyValue {
+ key!: K;
+ val!: V;
+
+ static readonly indexes = "key";
+}
+
+interface IKeyValue {
+ key: K;
+ val: V;
+}
+export function toObject(kvs: IKeyValue[]): Record {
+ return Object.fromEntries(kvs.map(({key, val}) => [key, val]));
+}
+export function toKeyValueArray(o: Record): IKeyValue[] {
+ return Object.entries(o).map(([key, val]) => ({ key, val: structuredClone(val) }));
+}
\ No newline at end of file
diff --git a/src/lib/delegate_presets/index.ts b/src/lib/delegate_presets/index.ts
index 87d0afd..f93124d 100644
--- a/src/lib/delegate_presets/index.ts
+++ b/src/lib/delegate_presets/index.ts
@@ -1,4 +1,15 @@
import type { DelegateAttrs } from "$lib/types";
+import DEFAULT_DELEGATES_JSON from "$lib/delegate_presets/preset-un.json";
+
+type DelProperties = Record;
+/**
+ * The object of default delegates.
+ */
+export const DEFAULT_DELEGATES: DelProperties = DEFAULT_DELEGATES_JSON;
+/**
+ * The key of the default preset.
+ */
+export const DEFAULT_PRESET_KEY = "un";
/**
* All defined presets.
@@ -16,22 +27,15 @@ export const PRESETS = {
*/
const NO_PRESET: readonly (keyof typeof PRESETS)[] = ["custom"];
-/**
- * @returns the default preset key (the first one defined on the object).
- */
-export function defaultPresetKey(): keyof typeof PRESETS {
- return (Object.keys(PRESETS) as (keyof typeof PRESETS)[])[0];
-}
-
/**
* Gets the preset data at a given file key.
* @param file the preset key (must be a key in `PRESETS`)
* @returns the preset data (if it exists)
* @throws if key is not in `PRESETS` or JSON is invalid or doesn't exist
*/
-export async function getPreset(key: keyof typeof PRESETS) {
+export async function getPreset(key: keyof typeof PRESETS): Promise {
if (!NO_PRESET.includes(key)) {
const { default: json } = await import(`$lib/delegate_presets/preset-${key}.json`);
- return structuredClone>(json);
+ return structuredClone(json);
}
}
diff --git a/src/lib/motions/definitions.ts b/src/lib/motions/definitions.ts
index c6a4468..97f39d7 100644
--- a/src/lib/motions/definitions.ts
+++ b/src/lib/motions/definitions.ts
@@ -1,8 +1,10 @@
-import type { DelegateAttrs, Motion, MotionKind, SortEntry } from "$lib/types";
+import type { Motion, MotionKind, SortEntry } from "$lib/types";
import { nonEmptyString, presentDelegateSchema, refineSpeakingTime, timeSchema, topicSchema } from "$lib/motions/form_validation";
import type { MotionInput } from "$lib/motions/types";
import { z } from "zod";
import { stringifyTime } from "$lib/util/time";
+import { type Delegate, findDelegate } from "$lib/db/delegates";
+import type { SessionDatabase } from "$lib/db/index.svelte";
/**
* The label/name given to each motion kind.
@@ -55,10 +57,10 @@ export const DEFAULT_SORT_PRIORITY: SortEntry[] = [
* Schema verification for a given form.
* This takes the form inputs and verifies & creates the motion object associated with the form.
*/
-export function createMotionSchema(delegates: Record, presentDelegates: string[]) {
+export function createMotionSchema(delegates: Delegate[]) {
const base = (k: K) => z.object({
id: nonEmptyString({ description: "ID" }),
- delegate: presentDelegateSchema(delegates, presentDelegates),
+ delegate: presentDelegateSchema(delegates),
kind: z.literal(k)
});
@@ -85,17 +87,11 @@ export function createMotionSchema(delegates: Record, pre
]) satisfies z.ZodType;
}
-/**
- * Defines how to convert a motion back into an input motion.
- * @param m
- * @param delAttrs
- * @returns
- */
-export function inputifyMotion(m: Motion, delAttrs: Record): MotionInput {
+function partialInputifyMotion(m: Motion): MotionInput {
if (m.kind === "mod") {
return {
id: m.id,
- delegate: delAttrs[m.delegate].name,
+ delegate: "",
kind: m.kind,
totalTime: stringifyTime(m.totalTime),
speakingTime: stringifyTime(m.speakingTime),
@@ -105,7 +101,7 @@ export function inputifyMotion(m: Motion, delAttrs: Record;
+export function inputifyMotion(m: Motion, delegates?: any): any {
+ const im = partialInputifyMotion(m);
+
+ // If session database:
+ if (typeof delegates === "object") {
+ return (delegates as SessionDatabase["delegates"]).get(m.delegate).then(d => {
+ if (d) im.delegate = d.name;
+ return im;
+ });
+ }
+
+ // If array:
+ if (delegates instanceof Array) {
+ im.delegate = findDelegate(delegates, m.delegate)?.name ?? "";
+ }
+ // If undefined:
+ return im;
}
\ No newline at end of file
diff --git a/src/lib/motions/form_validation.ts b/src/lib/motions/form_validation.ts
index da913cb..50905b4 100644
--- a/src/lib/motions/form_validation.ts
+++ b/src/lib/motions/form_validation.ts
@@ -1,4 +1,4 @@
-import type { DelegateAttrs } from "$lib/types";
+import type { Delegate } from "$lib/db/delegates";
import { parseTime } from "$lib/util/time";
import { z } from "zod";
@@ -25,30 +25,29 @@ export function formatValidationError(error: z.ZodError) {
* Creates a schema that requires the input is the name of a present delegate.
* This also transforms the input to the key of the delegate.
*
- * @param delegates record of delegates
- * @param presentDelegates the list of present delegates
+ * @param delegates record of delegates and their presence
* @returns the schema
*/
-export function presentDelegateSchema(delegates: Record, presentDelegates: string[]) {
+export function presentDelegateSchema(delegates: Delegate[]) {
return nonEmptyString({ description: "Delegate name", required_error: "Delegate name is a required field" })
.transform((name, ctx) => {
- const key = Object.keys(delegates).find(k => delegates[k].name.localeCompare(name, undefined, { sensitivity: "base" }) == 0);
- if (!key) {
+ const del = delegates.find(d => d.nameEquals(name));
+ if (!del) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `${name} is not a delegate`
})
return z.NEVER;
- } else if (!presentDelegates.includes(key)) {
+ } else if (!del.isPresent()) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
- message: `${delegates[key].name} is not a present delegate`
+ message: `${del.name} is not a present delegate`
})
return z.NEVER;
} else {
- return key;
+ return del.id;
}
})
}
diff --git a/src/lib/stores/README.md b/src/lib/stores/README.md
deleted file mode 100644
index d5d8c92..0000000
--- a/src/lib/stores/README.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Stores
-
-This folder defines stores, which handle cross-page and cross-refresh information.
-
-- `index.ts`: Boilerplate that allows stores to be created.
-- `session.ts`: Session data store
-- `settings.ts`: Settings store
-
-A given store can be created by calling the `createXXContext` function of a given store module in the top level of a `+layout.svelte` file. (For example, `createSessionDataContext` from `session.ts` is called in the top level of `dashboard/+layout.svelte`).
-
-If you wish to see the internals of how this is implemented, check out `index.ts`.
diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts
deleted file mode 100644
index a4a6e5b..0000000
--- a/src/lib/stores/index.ts
+++ /dev/null
@@ -1,154 +0,0 @@
-import { getContext, setContext } from "svelte";
-import { persisted } from "svelte-persisted-store";
-import type { Readable, Writable } from "svelte/store";
-import { SHADOW_ITEM_MARKER_PROPERTY_NAME as isDndShadowItem } from "svelte-dnd-action";
-import { getDndItemId } from "$lib/util/dnd";
-
-// For the purposes of this project, a store consists of:
-// 1. Writable properties
-// 2. Derived properties (properties that are calculated from other properties)
-// 3. Const properties (static properties that are generated once)
-// These three different kinds of properties are handled specifically by the type management below.
-
-/* TYPE STUFF */
-
-// This type stuff doesn't *really* need to be understood,
-// but it's used to facilitate easy typing for the other files in this folder.
-
-// These _KeysXX types are used to filter the keys of a given object type..
-/**
- * Gets all keys of Ctx whose values are of type Writable<_>
- */
-type _KeysWritable = { [K in keyof Ctx]: Ctx[K] extends Writable ? K : never }[keyof Ctx];
-/**
- * Gets all keys of Ctx whose values are of type Readable<_> (or Writable<_>)
- */
-type _KeysReadable = { [K in keyof Ctx]: Ctx[K] extends Readable ? K : never }[keyof Ctx];
-/**
- * Gets all keys of Ctx which represent a "default" property (1 in above definition)
- */
-type _KeysDefault = _KeysWritable;
-/**
- * Gets all keys of Ctx which represent a "derived" property (2 in above definition)
- */
-type _KeysDerived = Exclude<_KeysReadable, _KeysWritable>;
-/**
- * Gets all keys of Ctx which represent a "const" property (3 in above definition)
- */
-type _KeysConst = Exclude>;
-
-// These types split a given object into the 3 different items defined above.
-/**
- * Filters an object Ctx to only its "default" properties (def. 1)
- */
-type WDefaults = { [K in _KeysDefault]: Ctx[K] };
-/**
- * Filters an object Ctx to only its "default" properties (def. 1),
- * and strips the `Writable<_>` wrapper around each value type.
- */
-type Defaults = { [K in _KeysDefault]: Ctx[K] extends Writable ? V : never };
-/**
- * Filters an object Ctx to only its "derived" properties (def. 2)
- */
-type Derived = { [K in _KeysDerived]: Ctx[K] };
-/**
- * Filters an object Ctx to only its "const" properties (def. 3)
- */
-type Consts = { [K in _KeysConst]: Ctx[K] };
-
-/* END TYPE STUFF */
-
-/**
- * Creates all the useful functions and properties needed for a persistent store.
- *
- * Example:
- *
- * Suppose we wanted to create a `Settings` store
- * that we could access across pages and refreshes
- * with schema:
- * ```ts
- * // Suppose we wanted to create a store for:
- * type Settings = {
- * // writable properties
- * enableFoo: Writable,
- * enableBar: Writable,
- * // derived property (from the above)
- * fooAndBarEnabled: Readable,
- * // const property
- * baz: boolean
- * }
- * ```
- *
- * We could construct it like so:
- * ```
- * const { getDefaults, createContext, resetContext, getStoreContext } = createStore(
- * // defaults for all writable properties:
- * { enableFoo: false, enableBar: false },
- * // create all derived properties:
- * (data) => ({ fooAndBarEnabled: derived([enableFoo, enableBar], ([$foo, $bar]) => $foo && $bar) }),
- * // create all const properties:
- * () => ({ baz: true })
- * )
- * ```
- *
- * This function will automatically construct the persistent stores for each default property.
- * Each store can be created (in a `+layout.svelte`) with `createContext` and accessed with `getStoreContext`.
- *
- * Additionally, by specifying a type parameter, this function can give type-autocomplete support while
- * defining each of the three kinds of properties.
- *
- * @param key a unique key to use for Svelte `getContext`/`setContext` and when storing for persistence
- * @param defaults an object defining defaults for all writable fields (this can also be a function that defines all the defaults)
- * @param derived a function that takes all the defaults and const values and produces an object defining all derived properties
- * @param consts a function that defines all const properties
- * @returns an object holding a few fields:
- * - `getDefaults()`: a function that returns all of the defaults of the function
- * - `createContext()`: a function that initializes and creates the store (should be called in the top-level of a `+layout.svelte` file)
- * - `resetContext(ctx)`: a function that resets all writable properties of a store to their defaults
- * - `getStoreContext()`: a function returning the store (can also be accessed normally via `getContext(key)`)
- */
-export function createStore(
- key: string,
- defaults: Defaults | (() => Defaults),
- derived: (df: WDefaults & Consts) => Derived,
- consts: () => Consts
-) {
- const getDefaults = () => typeof defaults === "function" ? (defaults as () => Defaults)() : structuredClone(defaults);
- const createContext = () => {
- // Create defaults with writable fields:
- const dfCtx = Object.fromEntries(
- Object.entries(getDefaults())
- .map(([k, v]) => [k, persisted(`${key}.${k}`, v, {
- beforeRead(o) {
- // Process changes made due to svelte-dnd-action
- if (o instanceof Array) {
- for (let item of o) {
- if ("id" in item) {
- item.id = getDndItemId(item);
- }
- delete item[isDndShadowItem];
- delete item.originalId;
- }
- }
-
- return o;
- }
- })])
- ) as unknown as WDefaults;
-
- // Add consts to it:
- const dfcCtx = Object.assign(dfCtx, consts());
- // Add derived properties to it:
- const ctx = Object.assign(dfcCtx, derived(dfcCtx));
- return setContext(key, ctx);
- };
- const resetContext = (ctx: Ctx) => {
- for (let entry of Object.entries(getDefaults())) {
- let [key, dflt] = entry as unknown as [_KeysDefault, unknown];
- (ctx[key] as Writable).set(dflt);
- }
- }
- const getStoreContext = () => getContext(key);
-
- return { getDefaults, createContext, resetContext, getStoreContext };
-}
\ No newline at end of file
diff --git a/src/lib/stores/session.ts b/src/lib/stores/session.ts
deleted file mode 100644
index 2c25af5..0000000
--- a/src/lib/stores/session.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { derived } from "svelte/store";
-import { createAccessibleSettings } from "$lib/stores/settings";
-import type { SessionData } from "$lib/types";
-import { createStore } from ".";
-
-const { createContext, resetContext, getStoreContext } = createStore("sessionData",
- {
- delegateAttendance: {},
- motions: [],
- selectedMotion: null,
- speakersList: []
- },
- ({ delegateAttendance }) => ({
- presentDelegates: derived(delegateAttendance, $att => Object.keys($att).filter(k => $att[k] !== "NP"))
- }),
- () => ({
- settings: createAccessibleSettings()
- })
-)
-
-export {
- createContext as createSessionDataContext,
- resetContext as resetSessionDataContext,
- getStoreContext as getSessionDataContext
-};
\ No newline at end of file
diff --git a/src/lib/stores/settings.ts b/src/lib/stores/settings.ts
deleted file mode 100644
index f5853e3..0000000
--- a/src/lib/stores/settings.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-// We're assuming preset-un.json exists and is the default.
-import DEFAULT_DELEGATES from "$lib/delegate_presets/preset-un.json";
-import { DEFAULT_SORT_PRIORITY } from "$lib/motions/definitions";
-import type { AccessibleSettings, Settings } from "$lib/types";
-import { derived, readonly } from "svelte/store";
-import { createStore } from ".";
-
-const { getDefaults, createContext, resetContext, getStoreContext } = createStore("settings",
- {
- delegateAttributes: DEFAULT_DELEGATES,
- sortOrder: DEFAULT_SORT_PRIORITY,
- delegatesEnabled: Object.fromEntries(
- Object.keys(DEFAULT_DELEGATES).map(k => [k, true])
- ),
- title: "General Assembly",
- preferences: {
- enableMotionRoundRobin: true,
- enableMotionExt: true,
- pauseMainTimer: true,
- }
- },
- () => ({}),
- () => ({})
-);
-
-export {
- getDefaults,
- createContext as createSettingsContext,
- resetContext as resetSettingsContext,
- getStoreContext as getSettingsContext
-};
-
-/**
- * Creates an object which holds a view of the settings.
- *
- * This is created so that settings can be accessed using the session data store
- * whilst preventing accidental modifications to the settings
- * and simplifying unimportant details for the session.
- */
-export function createAccessibleSettings(): AccessibleSettings {
- const settings = getStoreContext();
- return {
- delegateAttributes: derived([settings.delegateAttributes, settings.delegatesEnabled],
- ([$attrs, $enables]) => Object.fromEntries(Object.entries($attrs).filter(([k]) => $enables[k] ?? false))
- ),
- sortOrder: readonly(settings.sortOrder),
- title: settings.title
- };
-}
\ No newline at end of file
diff --git a/src/lib/stores/stats.ts b/src/lib/stores/stats.ts
deleted file mode 100644
index 7a5b276..0000000
--- a/src/lib/stores/stats.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import type { StatsData, StatsDataStore } from "$lib/types";
-import { createStore } from ".";
-
-const { createContext, resetContext, getStoreContext } = createStore("statistics",
- {
- stats: {}
- },
- () => ({}),
- () => ({})
-)
-
-export function defaultStats(): StatsData {
- return {
- motionsProposed: 0,
- motionsAccepted: 0,
- timesSpoken: 0,
- durationSpoken: 0
- };
-}
-
-export function updateStats(stats: StatsDataStore["stats"], key: string | undefined, cb: (val: StatsData) => void) {
- if (typeof key === "string") stats.update($s => {
- $s[key] ??= defaultStats();
- cb($s[key]);
- return $s;
- });
-}
-export {
- createContext as createStatsContext,
- resetContext as resetStatsContext,
- getStoreContext as getStatsContext
-};
\ No newline at end of file
diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts
index 6703f81..390499c 100644
--- a/src/lib/types.d.ts
+++ b/src/lib/types.d.ts
@@ -1,5 +1,18 @@
import type { Readable, Writable } from "svelte/store";
+/**
+ * ID of a delegate
+ */
+export type DelegateID = number;
+/**
+ * ID of a given motion in the motion table.
+ */
+export type MotionID = string;
+/**
+ * ID of a given speaker slot in the speaker list.
+ */
+export type SpeakerEntryID = string;
+
/**
* Attributes each delegate in a given preset should have.
*
@@ -100,91 +113,84 @@ export type Preferences = {
}
/**
- * All configurable settings (in store format).
+ * All configurable settings.
*/
export type Settings = {
- /**
- * Delegate keys to characteristic data about the delegate (e.g., name and aliases)
- */
- delegateAttributes: Writable>,
-
/**
* The established sort order.
* Values first in the list are prioritized, with the order parameter handling ties.
*
* Any kinds not specified in this list are thrown at the end.
*/
- sortOrder: Writable,
+ sortOrder: SortEntry[],
- /**
- * Keys of delegates enabled for this assembly.
- */
- delegatesEnabled: Writable>,
-
/**
* The title of the assembly.
*/
- title: Writable,
+ title: string,
/**
* Toggleable preferences.
*/
- preferences: Writable
+ preferences: Preferences,
};
-/**
- * A wrapper type around settings to designate all accessible settings
- * in `SessionData`.
- */
-export type AccessibleSettings = {
- delegateAttributes: Readable>,
- sortOrder: Readable,
- title: Writable
-}
-
// Attendance
export type DelegatePresence = "NP" | "P" | "PV";
// Motions
+/**
+ * Data relating to a motion's properties.
+ */
export type Motion = {
- id: string,
- delegate: string,
+ id: MotionID,
+ delegate: DelegateID,
kind: "mod",
totalTime: number,
speakingTime: number,
topic: string,
isExtension: boolean
} | {
- id: string,
- delegate: string,
+ id: MotionID,
+ delegate: DelegateID,
kind: "unmod",
totalTime: number,
isExtension: boolean
} | {
- id: string,
- delegate: string,
+ id: MotionID,
+ delegate: DelegateID,
kind: "rr",
speakingTime: number,
topic: string
totalSpeakers: number
} | {
- id: string,
- delegate: string,
+ id: MotionID,
+ delegate: DelegateID,
kind: "other",
totalTime: number,
topic: string
};
export type MotionKind = Motion["kind"];
+/**
+ * Data relating to the current motion's operation.
+ */
+export type CurrentMotionState = {
+ /**
+ * Motion's speaker list (if it exists)
+ */
+ speakersList: Speaker[]
+};
+
export type Speaker = {
/**
* Identifier for this speaker entry.
*/
- id: string,
+ id: SpeakerEntryID,
/**
- * The key of the delegate.
+ * The key/delegate ID of the delegate.
*/
- key: string,
+ key: DelegateID,
/**
* Whether they have completed speaking.
*/
@@ -192,38 +198,77 @@ export type Speaker = {
};
// Session Data
+/**
+ * All data stored as "session data" in the database.
+ *
+ * This excludes delegate session data which is stored separately.
+ */
export type SessionData = {
- settings: AccessibleSettings,
+ sessionKey?: number,
/**
- * Attendance status of each delegate in the current session.
+ * All specified motions (from the points & motions page).
*/
- delegateAttendance: Writable>,
+ motions: Motion[],
/**
- * Derived attribute (based on delegateAttendance) that produces the list of present delegates.
+ * The motion that was selected (and is currently on display in the current motion page).
*/
- presentDelegates: Readable,
+ selectedMotion: Motion | null,
+ /**
+ * The state properties of the motion.
+ */
+ selectedMotionState: CurrentMotionState,
+ /**
+ * The speakers list and speaker attributes (such as whether the given speaker has spoken already)
+ */
+ speakersList: Speaker[],
+};
+export type DelSessionData = {
+ presence: DelegatePresence,
+ stats: StatsData
+};
+export type PrevSessionData = {
+ common: SessionData,
+ delegates: {
+ id: DelegateID,
+ session: DelSessionData
+ }[]
+};
+/**
+ * All data stored in the session context.
+ */
+export type SessionContext = {
+ /**
+ * Array of enabled delegates.
+ */
+ delegates: Readable,
+
/**
* All specified motions (from the points & motions page).
*/
- motions: Writable,
+ motions: Writable,
/**
* The motion that was selected (and is currently on display in the current motion page).
*/
- selectedMotion: Writable,
+ selectedMotion: Writable,
+ /**
+ * The state properties of the motion.
+ */
+ selectedMotionState: Writable,
/**
* The speakers list and speaker attributes (such as whether the given speaker has spoken already)
*/
- speakersList: Writable
-};
+ speakersList: Writable,
-// App bar data
-export type AppBarData = {
/**
- * Topic to display on the app bar
+ * Committee title, visible on the app bar.
*/
- topic: string | undefined
-}
+ barTitle: Writable,
+ /**
+ * Current topic of discussion, visible on the app bar.
+ */
+ barTopic: string | undefined,
+};
export type ClockMessage =
| { kind: "startTick", ts: number }
@@ -247,7 +292,4 @@ export type StatsData = {
* Total duration this delegate has gone up to speak (in speakers list and moderated caucuses), in milliseconds.
*/
durationSpoken: number
-}
-export type StatsDataStore = {
- stats: Writable>
-}
+}
\ No newline at end of file
diff --git a/src/lib/util/index.ts b/src/lib/util/index.ts
index b4b7a86..6310b23 100644
--- a/src/lib/util/index.ts
+++ b/src/lib/util/index.ts
@@ -32,23 +32,6 @@ export type Comparator = (a: K, b: K) => number;
*/
export const compare = ((a: any, b: any, reverse: boolean = false) => (reverse ? -1 : 1) * (a < b ? -1 : a > b ? 1 : 0)) satisfies Comparator;
-/**
- * Creates a new object where Maps each entry in the object to a new entry.
- * @param o the object
- * @param cb the callback
- * @returns the new object
- */
-export function mapObj<
- K extends keyof unknown, V,
- K1 extends keyof unknown, V1
->(o: Record, cb: (key: K, val: V, i: number) => [K1, V1]): Record {
- return Object.fromEntries(
- Object.entries(o)
- .map(([k, v], i) => cb(k as K, v, i)
- )
- );
-}
-
/**
* Allows user to download a file.
* @param filename The name of the file to download
@@ -110,4 +93,4 @@ export function lazyslide(node: HTMLElement, { delay = 0, duration = 400, easing
`${padProperty[1]}: ${t * (padBRValue ?? 0)}px;`;
}
} satisfies TransitionConfig;
-}
\ No newline at end of file
+}
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 39ddc54..8dc1a50 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -2,18 +2,14 @@
import "../app.css";
import { computePosition, autoUpdate, offset, shift, flip, arrow, size } from '@floating-ui/dom';
import { initializeStores, Modal, storePopup } from "@skeletonlabs/skeleton";
- import { createSettingsContext } from "$lib/stores/settings";
- import { createSessionDataContext } from "$lib/stores/session";
- import { createStatsContext } from "$lib/stores/stats";
+ import { createSessionContext } from "$lib/context/index.svelte";
let { children } = $props();
initializeStores();
storePopup.set({ computePosition, autoUpdate, offset, shift, flip, arrow, size });
- createSettingsContext();
- createSessionDataContext();
- const { stats } = createStatsContext();
+ createSessionContext();
function keydown(e: KeyboardEvent) {
// Allows ESC to be used to unfocus an element.
@@ -21,29 +17,13 @@
(document.activeElement as HTMLElement)?.blur?.();
}
}
-
- function storage(e: StorageEvent) {
- // HACK: Synchronize stats across different screens
- if (e.storageArea === localStorage && e.key === "statistics.stats") {
- if (e.newValue != null && e.oldValue !== e.newValue) {
- let json = undefined;
- try {
- json = JSON.parse(e.newValue);
- } catch (e) {
-
- }
-
- if (json) stats.set(json);
- }
- }
- }
{@render children()}
-
+