diff --git a/Extension/App/Options.tsx b/Extension/App/Options.tsx new file mode 100644 index 0000000..20051eb --- /dev/null +++ b/Extension/App/Options.tsx @@ -0,0 +1,126 @@ +// Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC + +import React, { useEffect, useReducer, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { ClockIcon, GearIcon } from '../View/Icons' +import { Tooltip } from '../View/Primitives' + +import { + CommandStructureInit, ModelConfigSync, + ModelConfigSyncGet, + ModelConfigSyncInit +} from '../Model' +import TimesheetsView from '../View/Timesheets' +import SettingsEditor from '../View/SettingsView' + +export function OptionsView () { + console.log('[OptionsView].Begin') + + enum Modes { + CentralCommand, + Estuary, + Init, + Intake, + Loading, + MissionControl, + Post, + Settings, + Timesheets + } + + const SettingsDispatcher = (state, action) => { + let type = action.type + console.log('[OptionsView.ReduceState]: ' + type) + switch (type) { + case null: return state + case 'Options': + return state.map((state_prior) => { + ///if (state_prior.id === action.id) { + /// return { ...state_prior, complete: !state_prior.complete }; + ///} else { + /// return state_prior; + ///} + return state_prior + }) + case 'OptionsUsernameSet': + return state.map((state_prior) => { + + }) + default: return state + } + }; + + const [CommandStructure, CommandStructureSet] = + useState(CommandStructureInit) + //const [Visible, VisibleSet] = useState(false) //??? + const [State, StateSet] = useState(0) + const [IsSaving, IsSavingSet] = useState(false) + const [Mode, ModeSet] = useState(Modes.Settings) + const [Config, ConfigSet] = + useState(ModelConfigSyncInit) + + const SaveButtonStyles = 'block mt-10 border-none outline-none' + + 'rounded-md p-4 bg-violet-500 font-bold' + + 'cursor-pointer'; + + useEffect(() => { + console.log('[useEffect]') + ModelConfigSyncGet().then(options_new => ConfigSet(options_new)) + }, []) + + const SettingsReducer = (action) => { + return SettingsDispatcher(Config, action) + } + const [AppState, Dispatch] = useReducer(SettingsReducer, { prop1: null, prop2: null}) + + if (Config == null || Config == undefined) return null + + return ( +
+
+
+
+
+ +
+
+
+ { Mode == Modes.Timesheets && + + + + }{ Mode != Modes.Timesheets && + + + + }{ Mode == Modes.Settings && + + + + }{ Mode != Modes.Settings && + + + + } +
+
+ { Mode == Modes.Timesheets && + + }{ Mode == Modes.Settings && + + } +
+
+
+
+ ) +} + +const container = document.createElement('div') +document.body.appendChild(container) +const root = createRoot(container) +root.render() diff --git a/Extension/App/Popup.tsx b/Extension/App/Popup.tsx new file mode 100644 index 0000000..b4520bd --- /dev/null +++ b/Extension/App/Popup.tsx @@ -0,0 +1,131 @@ +// Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC + +import React, { useEffect, useState } from 'react' +import { createRoot } from 'react-dom/client' +import { + ModelConfigSync, ModelConfigSyncGet, + ModelConfigSyncInit, + ModelConfigSyncSet, + ModelSyndicateGet +} from '../Model' +import MissionSelector from '../View/MissionSelector' +const { TimestampSeconds } = require('linearid') + +function SessionNumberChange(session_number: number, config: ModelConfigSync) { + if(session_number < 0) return + ModelConfigSyncSet({ ...config, session: session_number }) +} + +// Browser extension popup widget. +const Popup = () => { + console.log('>Popup') + // Live Coding AStartupMCC #39.F Add: Can clock and off to dummy... + const [Config, ConfigSet] = useState(ModelConfigSyncInit) + const [MissionHeading, MissionHeadingSet] = useState('') + const [SessionHeading, SessionHeadingSet] = useState('') + const [Syndicate, SyndicateSet ] = useState({}) + + const [IsSaving, IsSavingSet] = useState(false) + useEffect(() => { + console.log('[useEffect]') + ModelConfigSyncGet().then(options_new => ConfigSet(options_new)) + ModelSyndicateGet().then(syndicate_new => SyndicateSet(syndicate_new)) + }, []) + if (Config == null) return
Config == null
+ let { account, mission, mission_ids, repo, session, session_ids } = Config + if (session == undefined) return
Config members undefined
+ const [SessionNumber, SessionNumberSet] = useState(0) + + function MissionHeadingUpdate (focus_headline: string) { + MissionHeadingSet(focus_headline) + } + + function SessionHeadingUpdate (focus_headline: string) { + SessionHeadingSet(focus_headline) + } + + function TimesheetPunchHandle () { + if (session == undefined) return + const Time = TimestampSeconds() + const TimeText = new Date(Time * 1000) + if(session == 0) { // End Session + console.log('Clocking on to Session #' + SessionNumber + ' at ' + TimeText) + //@todo Integrate with GitHub to create Session Tickets. + const ConfigNew = {...(Config as ModelConfigSync), session: SessionNumber} + ModelConfigSyncSet(ConfigNew) + ConfigSet(ConfigNew) + } else if (session < 0) { // Stop Break + const S = -session + console.log('Stopping break from Session #' + S + ' at ' + TimeText) + const ConfigNew = {...(Config as ModelConfigSync), session: S} + ModelConfigSyncSet(ConfigNew) + ConfigSet(ConfigNew) + } else { // -> Session > 0 + console.log('Clocking off from Session #' + session + ' at ' + TimeText) + const ConfigNew = {...(Config as ModelConfigSync), session: 0} + ModelConfigSyncSet(ConfigNew) + ConfigSet(ConfigNew) + } + } + + function TimesheetBreakStartHandle () { + if (session == undefined) return + let time = TimestampSeconds() + console.log('Starting break from Session #' + session + ' at ' + new Date(time * 1000)) + const ConfigNew = {...(Config as ModelConfigSync), session: -session} + ModelConfigSyncSet(ConfigNew) + ConfigSet(ConfigNew) + } + + return
+ +
+

Account: {account} #{Math.abs(session)}

+

Repo: {repo}

+

Mission: #{Math.abs(mission)}

+

Heading: {session_ids ?? '' + mission_ids}

+
+ { session == 0 && <> + + SessionNumberSet(event.target.valueAsNumber) } + disabled={ IsSaving } + /> + + } + { session > 0 && <> + + + } + { session < 0 && + + } + { session == 0 &&
+ Session and Mission Heading: + SessionHeadingUpdate(event.target.value) } + disabled={ IsSaving } + /> + Mission heading: + MissionHeadingUpdate(event.target.value) } + disabled={ IsSaving } + /> +
+ } +
+
+} + +const container = document.createElement('div') +document.body.appendChild(container) +const root = createRoot(container) +root.render() diff --git a/Extension/Ctlr/Background.ts b/Extension/Ctlr/Background.ts index 0e53d24..9ad87ce 100644 --- a/Extension/Ctlr/Background.ts +++ b/Extension/Ctlr/Background.ts @@ -1,7 +1,7 @@ // Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC -import { CommandStructureInit, CommandStructureSet, ModelConfigInit, -ModelConfigGet, ModelConfigSet, ModelIssueInit, ModelIssueSet, +import { CommandStructureInit, CommandStructureSet, ModelConfigSyncInit, +ModelConfigSyncGet, ModelConfigSyncSet, ModelIssueInit, ModelIssueSet, ModelMissionInit, ModelMissionSet, ModelSessionInit, ModelSessionSet, ModelSyndicateInit, ModelSyndicateSet } from '../Model' @@ -25,7 +25,7 @@ chrome.alarms.onAlarm.addListener((alarm) => { // @todo Inspect video timestamp 3:38:30 for spooky shit. chrome.runtime.onInstalled.addListener(() => { - ModelConfigSet(ModelConfigInit) + ModelConfigSyncSet(ModelConfigSyncInit) CommandStructureSet(CommandStructureInit) ModelIssueSet(ModelIssueInit) ModelMissionSet(ModelMissionInit) diff --git a/Extension/Ctlr/Content.tsx b/Extension/Ctlr/Content.ts similarity index 100% rename from Extension/Ctlr/Content.tsx rename to Extension/Ctlr/Content.ts diff --git a/Extension/Model/Chrome.ts b/Extension/Model/Chrome.ts new file mode 100644 index 0000000..42cfce0 --- /dev/null +++ b/Extension/Model/Chrome.ts @@ -0,0 +1 @@ +// Copyright AStartup; license at https://github.com/AStartupMCC diff --git a/Extension/Model/Firefox.ts b/Extension/Model/Firefox.ts new file mode 100644 index 0000000..9798afa --- /dev/null +++ b/Extension/Model/Firefox.ts @@ -0,0 +1,2 @@ +// Copyright AStartup; license at https://github.com/AStartupMCC + diff --git a/Extension/Model/index.ts b/Extension/Model/index.ts index 50d131e..dbf3a3a 100644 --- a/Extension/Model/index.ts +++ b/Extension/Model/index.ts @@ -2,35 +2,33 @@ export const UsernameInit = 'CookingWithCale' -/* Data model config that do not get synced with the server. */ -export type ModelAppState = { +// Model configuration settings that get synced with the browser. +export type ModelConfigSync = { + account?: string //< Current account. + content_scripts: boolean //< Content scripts enabled. + me?: string //< The user's Username. + metric_units?: boolean //< Standard (true) or Imperial units. + mission: number //< Current mission number. + mission_ids?: string //< Current mission ID string. + repo?: string //< Current repo. + session?: number //< Current session number (zero means clocked out). + session_ids?: string //< The session ID string. + them?: string //< Currently selected syndicate member. } -// Options that when changed triggers the DOM to rerender? -// The design goal of this data structure is to not have any nested objects. -// The ModelState data is stored using Objects and thus the key is not stored -// in the sub-Object, so we store the key of the current Object we are working -// on. -export type ModelConfig = { - // App State - modal_visible: boolean //< Modal is visible flag. +// Local model configuration settings. +export type ModelConfigLocal = { modal_state: number //< State of the modal. // Options - content_scripts: boolean //< Content scripts enabled. - metric_units?: boolean //< Standard (true) or Imperial units. - me?: string //< The user's Username. - them?: string //< Currently selected syndicate member. - session?: number //< Current session number. - account?: string //< Current account. - repo?: string //< Current repo. - mission: string //< Current mission string. + modal_visible: boolean //< Modal is visible flag. } const SessionFocusLengthMax = 100 //< Max length of a session focus heading. // The Global App Model state. export type ModelState = { - config?: ModelConfig //< Model data config. + config_sync?: ModelConfigSync //< Synced model config settings. + config_local?: ModelConfigLocal //< Local model config settings. command_structure?: Object //< Meta model for the incident command structure. issue?: Object //< The current issue. mission?: Object //< The current mission. @@ -125,18 +123,24 @@ export type ACommandStructureNode = { export type ModelKeys = keyof ModelState -export const ModelConfigInit: ModelConfig = { - modal_visible: false, - modal_state: 0, +export const ModelConfigSyncInit: ModelConfigSync = { content_scripts: false, metric_units: true, me: UsernameInit, them: '', session: 0, + session_ids: '', account: 'AStarStartup', repo: 'AStartupMCC', - mission: '' + mission: 0, + mission_ids: '' +} + +export const ModelConfigLocalInit: ModelConfigLocal = { + modal_visible: false, + modal_state: 0, } + // Unpacks the account/repo#MissionNumber.ChildMission from the input string. export function MissionStringUnpack(input: string) { let state = 0 @@ -279,7 +283,7 @@ export const ModelSyndicateInit: Object = { "Type": "Org", "Repos": { "AStartupMCC": { - "issues": { + "issues_open": { "86": "Abilities.Add: Can set and crop background in OBS for thumbnail", "85": "ContextMenu.Add quick paste feature", "75": "ContextMenu.AddAbility Right click on GitHub issue tickets and add them to the current mission or set as the current mission", @@ -424,7 +428,7 @@ export const ModelSyndicateInit: Object = { } }, "KabukiStarship": { - "Type": "Person", + "Type": "Org", "Repos": { "Script2": { "issues_open": { @@ -633,28 +637,18 @@ export const ModelSyndicateInit: Object = { } } -/* -export const ModelStateInit: ModelState = { - command_structure: CommandStructureInit, - issue: {}, - mission: {}, - config: ModelConfigInit, - session: {}, - syndicate: ModelSyndicateInit -}*/ - -export function ModelConfigGet(): Promise { - const keys: ModelKeys[] = ['config'] +export function ModelConfigLocalGet(): Promise { + const keys: ModelKeys[] = ['config_local'] return new Promise((resolve) => { chrome.storage.local.get(keys, (state: ModelState) => { - resolve(state.config ?? ModelConfigInit) + resolve(state.config_local ?? ModelConfigLocalInit) }) }) } -export function ModelConfigSet(config: ModelConfig): Promise { +export function ModelConfigLocalSet(config: ModelConfigLocal): Promise { const Values: ModelState = { - config: config, + config_local: config, } return new Promise((resolve) => { chrome.storage.local.set(Values, () => { @@ -663,6 +657,26 @@ export function ModelConfigSet(config: ModelConfig): Promise { }) } +export function ModelConfigSyncGet(): Promise { + const keys: ModelKeys[] = ['config_sync'] + return new Promise((resolve) => { + chrome.storage.sync.get(keys, (state: ModelState) => { + resolve(state.config_sync ?? ModelConfigSyncInit) + }) + }) +} + +export function ModelConfigSyncSet(config: ModelConfigSync): Promise { + const Values: ModelState = { + config_sync: config, + } + return new Promise((resolve) => { + chrome.storage.sync.set(Values, () => { + resolve() + }) + }) +} + export function CommandStructureGet(): Promise { const keys: ModelKeys[] = ['command_structure'] return new Promise((resolve) => { diff --git a/Extension/View/Icons.tsx b/Extension/View/Icons.tsx index 2408dd7..f81b984 100644 --- a/Extension/View/Icons.tsx +++ b/Extension/View/Icons.tsx @@ -1,8 +1,14 @@ import React from 'react' -export function IconGear() { +export function GearIcon() { return } + +export function ClockIcon() { + return + + +} diff --git a/Extension/View/Issues.tsx b/Extension/View/Issues.tsx index 541bb05..883986d 100644 --- a/Extension/View/Issues.tsx +++ b/Extension/View/Issues.tsx @@ -1,6 +1,6 @@ // Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC -import { ModelSyndicateGet, ModelConfigGet, ModelConfigSet } from '../Model' +import { ModelSyndicateGet, ModelConfigSyncGet, ModelConfigSyncSet } from '../Model' export function Issues({props}) { diff --git a/Extension/View/MissionClock.tsx b/Extension/View/MissionClock.tsx new file mode 100644 index 0000000..a431e23 --- /dev/null +++ b/Extension/View/MissionClock.tsx @@ -0,0 +1,5 @@ +// Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC + +export default function MissionClock() { + +} diff --git a/Extension/View/MissionSelector.tsx b/Extension/View/MissionSelector.tsx index ec56ebe..c727e05 100644 --- a/Extension/View/MissionSelector.tsx +++ b/Extension/View/MissionSelector.tsx @@ -1,7 +1,9 @@ // Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC import React, { useState } from 'react' -import { ModelConfig, ModelConfigSet } from '../Model' +import { ModelConfigSync, ModelConfigSyncSet } from '../Model' + +const { LLIDNextHex } = require('linearid') // Checks if the issue_num_title starts off with #mission_number_string (i.e #123). export function IssueIsSelected(issue_num_title: string, mission_number_string: string) { @@ -9,7 +11,7 @@ export function IssueIsSelected(issue_num_title: string, mission_number_string: //if(MLength < 1 || issue_num_title.length <= MLength) return false; // Example issue_is_selected: "ABC" or "A123" or "#123 Working example" let issue_is_selected = issue_num_title[0] != '#' && MLength < 1 - && issue_num_title.length <= MLength + && issue_num_title.length <= MLength //if(!issue_is_selected) return issue_is_selected let i = 0 for(; i < MLength; ++i) { @@ -22,8 +24,8 @@ export function IssueIsSelected(issue_num_title: string, mission_number_string: } export default function MissionSelector(props: { - Config : ModelConfig - ConfigSet: (o: ModelConfig) => void + Config : ModelConfigSync + ConfigSet: (o: ModelConfigSync) => void Syndicate: object }) { @@ -91,7 +93,8 @@ export default function MissionSelector(props: { ConfigSet({...Config, account: e.target.value}) }} value={account}> { Object.keys(Syndicate).map((key) => ( - ))} @@ -104,12 +107,13 @@ export default function MissionSelector(props: { onChange={(e) => { console.log("Changing Repo:" + e.target.value) const ConfigNew = {...Config, account: Config.account, repo: e.target.value} - ModelConfigSet(ConfigNew).then(() => { + ModelConfigSyncSet(ConfigNew).then(() => { ConfigSet(ConfigNew) }) }} value={account}> { repos.map((key) => ( - ))} @@ -120,18 +124,19 @@ export default function MissionSelector(props: { -} \ No newline at end of file +} diff --git a/Extension/View/Options.tsx b/Extension/View/Options.tsx deleted file mode 100644 index bd1fadb..0000000 --- a/Extension/View/Options.tsx +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC - -import React, { useEffect, useReducer, useState } from 'react' -import { createRoot } from 'react-dom/client' - -import { - CommandStructureInit, ModelConfig, - ModelConfigInit, ModelConfigGet -} from '../Model' - -const OptionsView = () => { - console.log('[OptionsView].Begin') - - enum Modes { - Init, - Loading, - Options, - Intake, - CentralCommand, - MissionControl, - Estuary, - Post - } - - const [CommandStructure, CommandStructureSet] = - useState(CommandStructureInit) - const [Visible, VisibleSet] = useState(false) - const [State, StateSet] = useState(0) - const [IsSaving, IsSavingSet] = useState(false) - const [Mode, ModeSet] = useState(Modes.Options) - const [Options, OptionsSet] = - useState(ModelConfigInit) - - const SaveButtonStyles = 'block mt-10 border-none outline-none' - + 'rounded-md p-4 bg-violet-500 font-bold' - + 'cursor-pointer'; - - useEffect(() => { - console.log('[useEffect]') - ModelConfigGet().then(options_new => OptionsSet(options_new)) - }, []) - - const ReduceState = (state, action) => { - let type = action.type - console.log('[OptionsView.ReduceState]: ' + type) - switch (type) { - case 'Options': - return state.map((state_prior) => { - ///if (state_prior.id === action.id) { - /// return { ...state_prior, complete: !state_prior.complete }; - ///} else { - /// return state_prior; - ///} - return state_prior - }) - case 'OptionsUsernameSet': - return state.map((state_prior) => { - - }) - default: return state - } - }; - - const [AppState, Dispatch] = useReducer(ReduceState, { prop1: null, prop2: null}) - - if (Options == null || Options == undefined) return null - - return ( -
-
-
-
- -
-
- ) -} - -const container = document.createElement('div') -document.body.appendChild(container) -const root = createRoot(container) -root.render() - -/* - { Mode == Modes.Intake && - - - - }{ Mode != Modes.Intake && - - - - }{ Mode == Modes.CentralCommand && - - - - }{ Mode != Modes.CentralCommand && - - - - }{ Mode == Modes.MissionControl && - - - - }{ Mode != Modes.MissionControl && - - - - }{ Mode == Modes.Estuary && - - - - }{ Mode != Modes.Estuary && - - - - }{ Mode == Modes.Post && - - - - }{ Mode != Modes.Post && - - - - }{ Mode == Modes.Options && - - - - }{ Mode != Modes.Options && - - - - } -*/ \ No newline at end of file diff --git a/Extension/View/Popup.tsx b/Extension/View/Popup.tsx deleted file mode 100644 index c5db852..0000000 --- a/Extension/View/Popup.tsx +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC - -import React, { useEffect, useState } from 'react' -import { createRoot } from 'react-dom/client' -import MissionSelector from './MissionSelector' -import { ModelConfig, ModelConfigGet, ModelConfigInit, ModelSyndicateGet } from '../Model' - -const Popup = () => { - console.log('>Popup') - const [Config, ConfigSet] = useState(ModelConfigInit) - const [Syndicate, SyndicateSet ] = useState({}) - - const [IsSaving, IsSavingSet] = useState(false) - useEffect(() => { - console.log('[useEffect]') - ModelConfigGet().then(options_new => ConfigSet(options_new)) - ModelSyndicateGet().then(syndicate_new => SyndicateSet(syndicate_new)) - }, []) - if (Config == null) return
Options == null
- let { me, account, repo, mission } = Config - /* - const handleCityButtonClick = () => { - if (cityInput === '') { - return - } - const updatedCities = [...cities, cityInput] - setStoredCities(updatedCities).then(() => { - setCities(updatedCities) - setCityInput('') - }) - } - */ - - const SessionFocusChange = (username: string) => { - } - - function LogInOutHandle() { - - } - - - return
- -
-

Account: {account}

-

Repo: {repo}

-

Mission: {mission}

- SessionFocusChange(event.target.value) } - disabled={ IsSaving } - /> - -
-} - -const container = document.createElement('div') -document.body.appendChild(container) -const root = createRoot(container) -root.render() diff --git a/Extension/View/Primitives.tsx b/Extension/View/Primitives.tsx index fd09c88..1b49105 100644 --- a/Extension/View/Primitives.tsx +++ b/Extension/View/Primitives.tsx @@ -9,3 +9,12 @@ export function ButtonSave({ props, children }) { { children } } + +export function Tooltip({ children, title }) { + return
{children} +
+} \ No newline at end of file diff --git a/Extension/View/SettingsEditor.tsx b/Extension/View/SettingsView.tsx similarity index 84% rename from Extension/View/SettingsEditor.tsx rename to Extension/View/SettingsView.tsx index 6b37e0a..38c14dd 100644 --- a/Extension/View/SettingsEditor.tsx +++ b/Extension/View/SettingsView.tsx @@ -1,18 +1,18 @@ // Copyright AStartup; license at https://github.com/AStarStartup/AStartupMCC -import { ModelAppState, ModelConfig } from '../Model' -import React, { Dispatch, useState } from "react"; +import { ModelConfigSync } from '../Model' +import React, { useState } from "react"; const SettingsEditor = (props: { - options: ModelConfig, - dispatch: Dispatch, + dispatch: (action: Object | null) => ModelConfigSync, is_saving: boolean, }) => { - let { options, dispatch, is_saving } = props; - if (options == undefined || dispatch == undefined) return null; + let { dispatch, is_saving } = props; + if (dispatch == undefined) return null; + const options = dispatch(null) let { content_scripts, me, metric_units } = options; if (content_scripts == undefined || metric_units == undefined || - me == undefined) return null + me == undefined) return null const [ContentScripts, ContentScriptsSet] = useState(content_scripts == true ? 'Enabled' : 'Disabled') @@ -59,7 +59,7 @@ const SettingsEditor = (props: {