diff --git a/.current-config-dir b/.current-config-dir new file mode 120000 index 0000000..b7f44f5 --- /dev/null +++ b/.current-config-dir @@ -0,0 +1 @@ +/home/uwun/.config/ortfo \ No newline at end of file diff --git a/.gitignore b/.gitignore index 38ae772..9f1f465 100644 --- a/.gitignore +++ b/.gitignore @@ -41,7 +41,7 @@ public/build/ .config mock-projects/ -!mock-projects/*/.portfoliodb/ +!mock-projects/*/.ortfo/ !mock-projects/neptune/cover-arts/renders/square.png vite.config.ts.js diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 8ce1e7e..92d6cef 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,9 +5,9 @@ "label": "front", "type": "shell", "command": "pnpm frontend-dev", - "runOptions": { - "runOn": "folderOpen" - }, + // "runOptions": { + // "runOn": "folderOpen" + // }, "presentation": { "group": "dev" }, @@ -17,9 +17,9 @@ "label": "back", "type": "shell", "command": "pnpm backend-dev", - "runOptions": { - "runOn": "folderOpen" - }, + // "runOptions": { + // "runOn": "folderOpen" + // }, "presentation": { "group": "dev" } @@ -28,9 +28,9 @@ "label": "tests", "type": "shell", "command": "pnpm vitest watch", - "runOptions": { - "runOn": "folderOpen" - }, + // "runOptions": { + // "runOn": "folderOpen" + // }, "presentation": { "group": "tdd" } diff --git a/.wakatime-project b/.wakatime-project index c632296..26bac15 100644 --- a/.wakatime-project +++ b/.wakatime-project @@ -1 +1 @@ -ortfogui +ortfo diff --git a/Makefile b/Justfile similarity index 65% rename from Makefile rename to Justfile index b2978a4..4e2ba2e 100644 --- a/Makefile +++ b/Justfile @@ -1,18 +1,13 @@ -.PHONY: build, installers - build: -# Build frontend pnpm frontend-build -# Gather generated files statik -f -src=dist/ -# Build backend cd backend && go build -o ../ortfo install: mv ortfo ~/.local/bin/ installers: - $(MAKE) build + just build cd installers && ./create.sh format: @@ -21,14 +16,10 @@ format: prettier --write frontend/** --plugin-search-dir=. setup: -# Install frontend dependencies pnpm install || yarn install || npm install -# Install the statik tool go install github.com/rakyll/statik -# Prepare statik content mkdir -p dist/ statik -f -src=dist/ -# Install backend dependencies go mod tidy test: diff --git a/backend/database.go b/backend/database.go index 74fced6..06c35d3 100644 --- a/backend/database.go +++ b/backend/database.go @@ -4,10 +4,10 @@ import ( "encoding/json" "fmt" "os" + "strings" "github.com/mitchellh/go-homedir" ortfodb "github.com/ortfo/db" - ortfomk "github.com/ortfo/mk" ) func (settings *Settings) InitializeDatabase() error { @@ -16,7 +16,7 @@ func (settings *Settings) InitializeDatabase() error { return fmt.Errorf("couldn't create data directories: %w", err) } // initialize portfolio database - err = WriteIfNotExist(ConfigurationDirectory("portfolio-database", "database.json"), []byte("[]")) + err = WriteIfNotExist(ConfigurationDirectory("portfolio-database", "database.json"), []byte("{}")) if err != nil { return fmt.Errorf("couldn't initialize portfolio database file: %w", err) } @@ -48,7 +48,7 @@ func (settings *Settings) InitializeDatabase() error { return nil } -func (settings *Settings) LoadDatabase() (db ortfomk.Database, err error) { +func (settings *Settings) LoadDatabase() (db ortfodb.Database, err error) { // Check if projects folder exists projectsFolder, err := homedir.Expand(settings.ProjectsFolder) if err != nil { @@ -64,25 +64,27 @@ func (settings *Settings) LoadDatabase() (db ortfomk.Database, err error) { } println("Not re-building database...") - return ortfomk.LoadDatabase(ConfigurationDirectory("portfolio-database")) + return ortfodb.LoadDatabase(ConfigurationDirectory("portfolio-database", "database.json"), true) } func (settings *Settings) RebuildDatabase() error { os.Chdir(ConfigurationDirectory("portfolio-database")) LogToBrowser("Rebuilding database...") + LogToBrowser(fmt.Sprintf("context is %#v", ctx)) projectsFolder, err := homedir.Expand(settings.ProjectsFolder) if err != nil { return fmt.Errorf("while expanding ~: %w", err) } - ortfodbConfig, err := ortfodb.NewConfiguration(ConfigurationDirectory("ortfodb.yaml"), ConfigurationDirectory("portfolio-database")) + ortfodbConfig, err := ortfodb.NewConfiguration(ConfigurationDirectory("ortfodb.yaml")) if err != nil { return fmt.Errorf("couldn't load database configuration: %w", err) } - go ortfodb.BuildAll( + LogToBrowser("building with ctx %#v", ctx) + ctx.BuildAll( projectsFolder, ConfigurationDirectory("portfolio-database", "database.json"), - ortfodb.Flags{Scattered: true, Silent: true, ProgressFile: ConfigurationDirectory("portfolio-database", "progress.json")}, + ortfodb.Flags{Scattered: true, Silent: true, ProgressInfoFile: ConfigurationDirectory("progress.jsonl")}, ortfodbConfig, ) if crash := recover(); crash != nil { @@ -91,15 +93,14 @@ func (settings *Settings) RebuildDatabase() error { if err != nil { return fmt.Errorf("couldn't build the portfolio's database: %w", err) } - LogToBrowser("Finish rebuilding database") return nil } func (settings *Settings) DeleteWorks(ids []string) error { var err error for _, id := range ids { - LogToBrowser("Deleting %s", JoinPaths(settings.ProjectsFolder, id, ".portfoliodb")) - err = os.RemoveAll(JoinPaths(settings.ProjectsFolder, id, ".portfoliodb")) + LogToBrowser("Deleting %s", JoinPaths(settings.ProjectsFolder, id, ".ortfo")) + err = os.RemoveAll(JoinPaths(settings.ProjectsFolder, id, ".ortfo")) if err != nil { ErrorToBrowser(err.Error()) return err @@ -108,9 +109,9 @@ func (settings *Settings) DeleteWorks(ids []string) error { return nil } -func (settings *Settings) ProgressFile() ortfomk.ProgressFile { - var progressFile ortfomk.ProgressFile - progressFilePath := ConfigurationDirectory("portfolio-database", "progress.json") +func (settings *Settings) ProgressFile() ortfodb.ProgressInfoEvent { + var progressFile ortfodb.ProgressInfoEvent + progressFilePath := ConfigurationDirectory("progress.jsonl") if _, err := os.Stat(progressFilePath); os.IsNotExist(err) { return progressFile } @@ -118,11 +119,14 @@ func (settings *Settings) ProgressFile() ortfomk.ProgressFile { if string(raw) == "" { return settings.ProgressFile() } - LogToBrowser("Progress file raw is %s", string(raw)) + // keep only the last line if err != nil { ErrorToBrowser("Couldn't read progress file: %s", err) return progressFile } + // Keep only the last line + lines := strings.Split(string(raw), "\n") + raw = []byte(lines[len(lines)-2]) err = json.Unmarshal(raw, &progressFile) if err != nil { ErrorToBrowser("Couldn't parse progress file: %s. Raw was %q", err, string(raw)) diff --git a/backend/main.go b/backend/main.go index 59ebf6f..3208b26 100644 --- a/backend/main.go +++ b/backend/main.go @@ -9,7 +9,6 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/mitchellh/go-homedir" ortfodb "github.com/ortfo/db" - ortfomk "github.com/ortfo/mk" "github.com/skratchdot/open-golang/open" "github.com/sqweek/dialog" "github.com/webview/webview" @@ -28,7 +27,7 @@ type PickFileConstraint struct { } var w webview.WebView -var ctx ortfodb.RunContext +var ctx *ortfodb.RunContext var settings Settings var Port = randomAvailablePort() @@ -53,14 +52,15 @@ func main() { func newOrtfoContext() error { var err error - ortfodbConfig, err := ortfodb.NewConfiguration(ConfigurationDirectory("ortfodb.yaml"), ConfigurationDirectory("portfolio-database")) + // ortfodbConfig, err := ortfodb.NewConfiguration(ConfigurationDirectory("ortfodb.yaml")) + ortfodbConfig, err := settings.InitializeOtfodbConfig() if err != nil { return fmt.Errorf("couldn't load database configuration: %w", err) } ctx, err = ortfodb.PrepareBuild(projectsFolder(), ConfigurationDirectory("portfolio-database", "database.json"), ortfodb.Flags{ - Scattered: true, - Silent: true, - ProgressFile: ConfigurationDirectory("portfolio-database", "progress.json"), + Scattered: true, + Silent: true, + ProgressInfoFile: ConfigurationDirectory("progress.jsonl"), }, ortfodbConfig) if err != nil { return fmt.Errorf("while preparing ortfodb build context: %w", err) @@ -69,6 +69,9 @@ func newOrtfoContext() error { } func startWebview() error { + ortfodb.LogFilePath = ConfigurationDirectory("ortfodb.log") + ortfodb.PrependDateToLogs = true + ortfodb.ReleaseBuildLock(ConfigurationDirectory("portfolio-database", "database.json")) err := newOrtfoContext() if err != nil { return err @@ -77,6 +80,9 @@ func startWebview() error { w = webview.New(true) defer w.Destroy() w.SetTitle("ortfo") + if os.Getenv("DEV") == "yes" { + w.SetTitle("ortfo [dev]") + } w.SetSize(800, 600, webview.HintMin) w.Navigate("http://localhost:" + func() string { if os.Getenv("DEV") == "yes" { @@ -106,25 +112,21 @@ func startWebview() error { w.Terminate() return nil }) - w.Bind("backend__databaseRead", func() (ortfomk.Database, error) { + w.Bind("backend__databaseRead", func() (ortfodb.Database, error) { return settings.LoadDatabase() }) w.Bind("backend__rebuildDatabase", func() error { return settings.RebuildDatabase() }) w.Bind("backend__rebuildWork", func(workID string) error { - newOrtfoContext() - return ortfodb.BuildSome(workID, projectsFolder(), ctx.OutputDatabaseFile, ctx.Flags, *ctx.Config) - }) - w.Bind("backend__analyzeMedia", func(workID string, mediaEmbed ortfodb.MediaEmbedDeclaration) (ortfodb.Media, error) { - settings, _ := LoadSettings() - projectsFolder, err := homedir.Expand(settings.ProjectsFolder) - if err != nil { - return ortfodb.Media{}, fmt.Errorf("while expanding projects folder: %w", err) + if workID == "" { + return fmt.Errorf("workID is empty") } - _, media, err := (&ortfodb.RunContext{ - DatabaseDirectory: projectsFolder, - }).AnalyzeMediaFile(workID, mediaEmbed) + _, err := ctx.BuildSome(workID, projectsFolder(), ctx.OutputDatabaseFile, ctx.Flags, *ctx.Config) + return err + }) + w.Bind("backend__analyzeMedia", func(workID string, mediaEmbed ortfodb.Media) (ortfodb.Media, error) { + _, media, _, err := ctx.AnalyzeMediaFile(workID, mediaEmbed) if err != nil { return ortfodb.Media{}, fmt.Errorf("while analyzing media: %w", err) } @@ -138,7 +140,7 @@ func startWebview() error { return Writeback(settings, description, workID) }) - w.Bind("backend__writeTags", func(tags []ortfomk.Tag) error { + w.Bind("backend__writeTags", func(tags []ortfodb.Tag) error { spew.Dump(tags) tagsBytes, err := yaml.Marshal(tags) if err != nil { @@ -147,7 +149,7 @@ func startWebview() error { return os.WriteFile(ConfigurationDirectory("portfolio-database", "tags.yaml"), tagsBytes, 0644) }) - w.Bind("backend__writeTechnologies", func(technologies []ortfomk.Technology) error { + w.Bind("backend__writeTechnologies", func(technologies []ortfodb.Technology) error { tagsBytes, err := yaml.Marshal(technologies) if err != nil { ErrorToBrowser(fmt.Sprintf("while converting to YAML: %v", err)) @@ -156,7 +158,7 @@ func startWebview() error { return os.WriteFile(ConfigurationDirectory("portfolio-database", "technologies.yaml"), tagsBytes, 0644) }) - w.Bind("backend__writeExternalSites", func(externalSites []ortfomk.ExternalSite) error { + w.Bind("backend__writeExternalSites", func(externalSites []ExternalSite) error { sitesBytes, err := yaml.Marshal(externalSites) if err != nil { return fmt.Errorf("while converting to YAML: %w", err) @@ -164,8 +166,8 @@ func startWebview() error { return os.WriteFile(ConfigurationDirectory("portfolio-database", "sites.yaml"), sitesBytes, 0644) }) - w.Bind("backend__writeCollection", func(collections []ortfomk.Collection) error { - collectionsByID := make(map[string]ortfomk.Collection) + w.Bind("backend__writeCollection", func(collections []Collection) error { + collectionsByID := make(map[string]Collection) for _, c := range collections { collectionsByID[c.ID] = c } @@ -191,7 +193,7 @@ func startWebview() error { return settings.LoadUIState() }) - w.Bind("backend__getBuildProgress", func() ortfomk.ProgressFile { + w.Bind("backend__getBuildProgress", func() ortfodb.ProgressInfoEvent { settings, _ := LoadSettings() return settings.ProgressFile() }) @@ -256,7 +258,7 @@ func startWebview() error { if err != nil { return "", fmt.Errorf("while loading settings: %w", err) } - bytes, err := os.ReadFile(JoinPaths(settings.ProjectsFolder, workID, ".portfoliodb", "description.md")) + bytes, err := os.ReadFile(JoinPaths(settings.ProjectsFolder, workID, ".ortfo", "description.md")) return string(bytes), err }) w.Bind("backend__writeRawDescription", func(workID string, content string) error { @@ -265,7 +267,7 @@ func startWebview() error { return fmt.Errorf("while loading settigns: %w", err) } - return os.WriteFile(JoinPaths(settings.ProjectsFolder, workID, ".portfoliodb", "description.md"), []byte(content), 0644) + return os.WriteFile(JoinPaths(settings.ProjectsFolder, workID, ".ortfo", "description.md"), []byte(content), 0644) }) w.Bind("backend__clearThumbnails", func() error { return os.RemoveAll(ConfigurationDirectory("portfolio-database", "media")) @@ -320,21 +322,15 @@ func Initialize() error { return fmt.Errorf("couldn't save default settings: %w", err) } + _, err = settings.InitializeOtfodbConfig() + if err != nil { + return fmt.Errorf("couldn't initialize configuration file: %w", err) + } + err = settings.InitializeDatabase() if err != nil { return fmt.Errorf("couldn't initialize portfolio database: %w", err) } - ortfomk.WarmUp(&ortfomk.GlobalData{ - Flags: ortfomk.Flags{ - ProgressFile: ConfigurationDirectory(".progress.json"), - Silent: true, - }, - // TODO make configurable - OutputDirectory: ConfigurationDirectory("built"), - // TODO #4 make configurable - TemplatesDirectory: ConfigurationDirectory("templates"), - HTTPLinks: make(map[string][]string), - }) return nil } diff --git a/backend/media.go b/backend/media.go index 3fd67a7..3516637 100644 --- a/backend/media.go +++ b/backend/media.go @@ -22,7 +22,7 @@ func (m mediaRoot) Open(name string) (http.File, error) { name = strings.TrimPrefix(name, "/") command, path, _ := strings.Cut(name, "/") path = filepath.Clean(path) - println(path, name) + println(command, path) switch command { case "projects": return http.Dir(m.projectsRoot).Open(path) diff --git a/backend/metadata.go b/backend/metadata.go new file mode 100644 index 0000000..0dd6f22 --- /dev/null +++ b/backend/metadata.go @@ -0,0 +1,19 @@ +package main + +type Localized[T any] map[string]T + +type ExternalSite struct { + Name string `yaml:"name"` + URL string `yaml:"url"` + Purpose string `yaml:"purpose,omitempty"` + Username string `yaml:"username,omitempty"` +} + +type Collection struct { + ID string `yaml:"-"` + Title Localized[string] `yaml:"title"` + Includes string `yaml:"includes"` + Description Localized[string] `yaml:"description"` + Singular string `yaml:"singular"` + Plural string `yaml:"plural"` +} diff --git a/backend/settings.go b/backend/settings.go index ae6a508..5fb6c6a 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -7,6 +7,8 @@ import ( "path/filepath" "github.com/cloudfoundry-attic/jibber_jabber" + ortfodb "github.com/ortfo/db" + "gopkg.in/yaml.v3" ) var ThemeNames = [...]string{"dark", "light"} @@ -139,11 +141,6 @@ func InitializeConfigurationDirectory() error { return fmt.Errorf("couldn't create configuration directory: %w", err) } - err = WriteIfNotExist(ConfigurationDirectory("ortfodb.yaml"), []byte("")) - if err != nil { - return fmt.Errorf("couldn't initialize database settings file: %w", err) - } - return nil } @@ -203,3 +200,31 @@ func (settings *Settings) LoadUIState() (state UIState, err error) { return } + +func (settings *Settings) InitializeOtfodbConfig() (ortfodb.Configuration, error) { + if settings == nil { + settings_, err := LoadSettings() + if err != nil { + return ortfodb.Configuration{}, fmt.Errorf("while loading settings: %w", err) + } + settings = &settings_ + } + // try to validate current ortfodb settings. if ok, don't touch them. + config, err := ortfodb.NewConfiguration(ConfigurationDirectory("ortfodb.yaml")) + if err == nil { + return config, nil + } + config = ortfodb.DefaultConfiguration() + config.ProjectsDirectory = settings.ProjectsFolder + encoded, err := yaml.Marshal(config) + if err != nil { + return config, fmt.Errorf("while encoding configuration to YAML: %w", err) + } + + err = os.WriteFile(ConfigurationDirectory("ortfodb.yaml"), encoded, 0644) + if err != nil { + return config, fmt.Errorf("while writing ortfodb.yaml: %w", err) + } + + return config, nil +} diff --git a/backend/site.go b/backend/site.go index 6d98aa5..c285adf 100644 --- a/backend/site.go +++ b/backend/site.go @@ -1,3 +1,4 @@ package main // func BuildSite() + diff --git a/backend/utils.go b/backend/utils.go index 40eb80d..3515722 100644 --- a/backend/utils.go +++ b/backend/utils.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/mitchellh/go-homedir" - ortfodb "github.com/ortfo/db" ) func prepareQuotedString(message string, a ...interface{}) string { @@ -40,25 +39,25 @@ func JoinPaths(paths ...string) string { return result } -func LanguagesIn(description ortfodb.ParsedWork) (languages []string) { - languages = make([]string, 0) - for lang := range description.Title { - languages = append(languages, lang) - } - for lang := range description.Paragraphs { - languages = append(languages, lang) - } - for lang := range description.Footnotes { - languages = append(languages, lang) - } - for lang := range description.Links { - languages = append(languages, lang) - } - for lang := range description.MediaEmbedDeclarations { - languages = append(languages, lang) - } - return -} +// func LanguagesIn(description ortfodb.ParsedWork) (languages []string) { +// languages = make([]string, 0) +// for lang := range description.Title { +// languages = append(languages, lang) +// } +// for lang := range description.Paragraphs { +// languages = append(languages, lang) +// } +// for lang := range description.Footnotes { +// languages = append(languages, lang) +// } +// for lang := range description.Links { +// languages = append(languages, lang) +// } +// for lang := range description.MediaEmbedDeclarations { +// languages = append(languages, lang) +// } +// return +// } // changeKeys changes the entries of m to replace its keys with the new keys described by replaceMap. // if the new key is the empty string, the corresponding entry is deleted. diff --git a/backend/writeback.go b/backend/writeback.go index 773174b..cf1e142 100644 --- a/backend/writeback.go +++ b/backend/writeback.go @@ -12,12 +12,12 @@ func Writeback(settings Settings, parsedDescription ortfodb.AnalyzedWork, workID // Put spaces back in metadata properties that should have them. // It also removes technical metadata properties that shouldn't be written back. // TODO: this behavior should be implemented in ortfo/mk. - description, err := ortfodb.ReplicateDescription(parsedDescription) + description, err := ctx.ReplicateDescription(parsedDescription) // println("Replicated description:", description) if err != nil { return fmt.Errorf("while replicating description: %w", err) } - writeTo := JoinPaths(settings.ProjectsFolder, workID, ".portfoliodb", "description.md") + writeTo := JoinPaths(settings.ProjectsFolder, workID, ".ortfo", "description.md") err = os.MkdirAll(filepath.Dir(writeTo), 0755) if err != nil { return fmt.Errorf("couldn't create missing directories: %w", err) diff --git a/frontend/App.svelte b/frontend/App.svelte index 38ad510..24ecc99 100644 --- a/frontend/App.svelte +++ b/frontend/App.svelte @@ -1,29 +1,38 @@ + + {#if import.meta.env.DEV } + ortfodb [dev] + {/if} + + {#await load()}

Sit tight, loading your stuff…

@@ -119,7 +141,11 @@ settings.subscribe(settings => applyTheme(settings.theme)) {:else} -
{JSON.stringify($debugFlyoutContent, null, 2)}
+
{JSON.stringify(
+						$debugFlyoutContent,
+						null,
+						2,
+					)}
{#if $state.openTab == "works"} @@ -190,7 +216,9 @@ settings.subscribe(settings => applyTheme(settings.theme)) font-variation-settings: "wght" 400; } -:global(*:not(input):not(select):not(textarea):not(.ProseMirror):focus-visible) { +:global( + *:not(input):not(select):not(textarea):not(.ProseMirror):focus-visible + ) { outline: 1px solid var(--ortforange); } @@ -233,7 +261,10 @@ settings.subscribe(settings => applyTheme(settings.theme)) transition: all 0.25s ease; } -:global(button[data-variant="inline"]:not(:disabled):hover, button[data-variant="inline"]:not(:disabled):focus) { +:global( + button[data-variant="inline"]:not(:disabled):hover, + button[data-variant="inline"]:not(:disabled):focus + ) { background-color: var(--ortforange); color: black; } @@ -293,7 +324,12 @@ settings.subscribe(settings => applyTheme(settings.theme)) :global(input:hover, textarea:hover, select:hover, ._markdown-editor:hover) { border-radius: 0; } -:global(input:focus, textarea:focus, select:focus-visible, ._markdown-editor:focus-within) { +:global( + input:focus, + textarea:focus, + select:focus-visible, + ._markdown-editor:focus-within + ) { border-radius: 0; border-color: var(--ortforange); } diff --git a/frontend/actions.ts b/frontend/actions.ts index 3f49ea0..702308a 100644 --- a/frontend/actions.ts +++ b/frontend/actions.ts @@ -13,26 +13,50 @@ export const helptip = ( } } +function tooltipProps(parameters: string | [string, number] | object | undefined) { + let content: string; + let delay = 50; + if (!parameters) { + return { + content: '', + // allowHtml: false, + delay: [delay, 0], + }; + } + + if (typeof parameters === 'string') content = parameters; + else if (Array.isArray(parameters)) [content, delay] = parameters; + else return { content: '', allowHTML: false, delay: [delay, 0], ...parameters }; + + return { content: content, allowHTML: true, delay: [delay, 0] }; +} + export const tooltip = ( - node: HTMLElement, - parameters: string | [string, number] | Object + node: HTMLElement & { + _tippy?: { + destroy: () => void; + setProps: (props: unknown) => void; + }; + }, + parameters: string | [string, number] | object | undefined, ) => { - let content: string - let delay: number = 50 - if (parameters === "") { - return - } - if (typeof parameters === "string") { - content = parameters - } else if (Array.isArray(parameters)) { - ;[content, delay] = parameters - } else { - // node.title = parameters.content - return tippy(node, parameters) - } - // node.title = content - return tippy(node, { content, delay: [delay, 0] }) -} + const properties = tooltipProps(parameters); + tippy(node, properties); + if (properties.content.length <= 0) node._tippy?.destroy(); + + return { + update(parameters: string | [string, number] | object | undefined) { + const properties = tooltipProps(parameters); + if (!node._tippy) tippy(node, properties); + if (properties.content.length <= 0) node._tippy?.destroy(); + node._tippy?.setProps(properties); + }, + destroy() { + node._tippy?.destroy(); + }, + }; +}; + export const scrollStates = ( element: HTMLElement, diff --git a/frontend/backend.ts b/frontend/backend.ts index 1abaaad..f79bd5a 100644 --- a/frontend/backend.ts +++ b/frontend/backend.ts @@ -1,17 +1,8 @@ -import { applyAbbreviations, collectAbbreviations } from "./description" -import type { - Database, - ExternalSite, - LayedOutElement, - ParsedDescription, - Tag, - Technology, - Translated, - Collection, -} from "./ortfo" +import type { AnalyzedWork, Collection, Database, ExternalSite } from "./ortfo" import type { Settings, State } from "./stores" import { lowercaseFirstCharacter, lowercaseNoSpacesKeys } from "./utils" - +import type { Tags as Tag } from "@ortfo/db/dist/tags" +import type { Technologies as Technology } from "@ortfo/db/dist/technologies" /* * Strings that represent binary files, with a filetype at the start. * Can be used in [src] attributes of images, for example. @@ -31,17 +22,11 @@ export type DirEntry = { export type MaybeError = string | null export type BuildProgress = { - total: number - processed: number - percent: number - current: { - id: string - step: string - resolution: number - file: string - language: string - output: string - } + works_done: number + works_total: number + work_id: string + phase: string + details: string[] } export type PickFileConstraint = { @@ -83,17 +68,29 @@ export const backend = { }, // ../backend/main.go databaseRead: async () => { - return lowercaseNoSpacesKeys( + const db = // @ts-ignore backend__* functions are injected by webview (from the backend) - (await backend__databaseRead()) || {} - ) as Database + ((await backend__databaseRead()) || {}) as Database + // TODO understand why madeWith can be null instead of [] + return Object.fromEntries( + Object.entries(db).map(([id, work]) => [ + id, + { + ...work, + metadata: { + ...work.metadata, + madeWith: work.metadata.madeWith || [], + }, + }, + ]), + ) }, // ../backend/main.go getBuildProgress: async () => { - return lowercaseNoSpacesKeys( + return ( // @ts-ignore backend__* functions are injected by webview (from the backend) - (await backend__getBuildProgress()) || {} - ) as BuildProgress + ((await backend__getBuildProgress()) || {}) as BuildProgress + ) }, // ../backend/main.go rebuildDatabase: async () => { @@ -103,15 +100,12 @@ export const backend = { // ../backend/main.go rebuildWork: async (id: string) => { // @ts-ignore backend__* functions are injected by webview (from the backend) - return (await backend__rebuildDatabase(id)) as MaybeError + return (await backend__rebuildWork(id)) as MaybeError }, // ../backend/main.go - writeToDisk: async (work: ParsedDescription, workID: string) => { + writeToDisk: async (work: AnalyzedWork, workID: string) => { // @ts-ignore backend__* functions are injected by webview (from the backend) - return (await backend__writeback( - applyAbbreviations(work), - workID - )) as MaybeError + return (await backend__writeback(work, workID)) as MaybeError }, // ../backend/main.go saveUIState: async (state: State) => { @@ -128,7 +122,7 @@ export const backend = { listDirectory: async (path: string) => { return lowercaseNoSpacesKeys( // @ts-ignore backend__* functions are injected by webview (from the backend) - await backend__listDirectory(path) + await backend__listDirectory(path), ) as DirEntry[] }, // ../backend/main.go @@ -140,8 +134,8 @@ export const backend = { Plural: t.plural, Description: t.description, Aliases: t.aliases, - LearnMoreURL: t.learnmoreurl, - })) + LearnMoreURL: t["learn more at"], + })), )) as MaybeError }, // ../backend/main.go @@ -179,7 +173,7 @@ export const backend = { title, startIn, constraint, - relativeTo + relativeTo, )) as string }, // ../backend/main.go:182 @@ -197,7 +191,7 @@ export const backend = { // @ts-ignore backend__* functions are injected by webview (from the backend) return (await backend__writeRawDescription( workID, - content + content, )) as MaybeError }, // ../backend/main.go @@ -212,7 +206,7 @@ export const backend = { extractColors: async (imagePath: string) => { return lowercaseNoSpacesKeys( // @ts-ignore backend__* functions are injected by webview (from the backend) - await backend__extractColors(imagePath) + await backend__extractColors(imagePath), ) as { primary: string secondary: string diff --git a/frontend/components/Card.svelte b/frontend/components/Card.svelte index 53cd7fe..647ba3a 100644 --- a/frontend/components/Card.svelte +++ b/frontend/components/Card.svelte @@ -17,8 +17,8 @@ const emit = createEventDispatcher() class:selectable class:selected class:has-icon={hasIcon} - on:click={() => { - if (clickable) emit("click") + on:click={(e) => { + if (clickable) emit("click", e) }} > {#if selectable} @@ -61,7 +61,9 @@ const emit = createEventDispatcher() .card.clickable { cursor: pointer; - transition: all 0.25s ease, background-color 0.5s ease; + transition: + all 0.25s ease, + background-color 0.5s ease; } .card.clickable:not(.creates):hover { background-color: var(--ortforange-light); diff --git a/frontend/components/CardContentBlock.svelte b/frontend/components/CardContentBlock.svelte index b37a784..e7217f5 100644 --- a/frontend/components/CardContentBlock.svelte +++ b/frontend/components/CardContentBlock.svelte @@ -6,7 +6,7 @@ import MarkdownEditor from "./MarkdownEditor.svelte" import { clickOutside } from "../actions" import { scale } from "svelte/transition" import { settings } from "../stores" -import type { WorkOneLang } from "../ortfo" +import type { AnalyzedWorkLocalized } from "../ortfo" import { _ } from "svelte-i18n" import { tooltip } from "../actions" import ContentBlockMedia from "./ContentBlockMedia.svelte" @@ -14,7 +14,7 @@ import ContentBlockMedia from "./ContentBlockMedia.svelte" const dispatch = createEventDispatcher() export let block: ContentBlock -export let work: WorkOneLang +export let work: AnalyzedWorkLocalized export let activeBlock: string let choosingWhatToInsert: boolean = false @@ -41,7 +41,7 @@ let choosingWhatToInsert: boolean = false
(activeBlock = block.id)} on:blur={() => (activeBlock = null)} placeholder={$_("name your link")} diff --git a/frontend/components/CardWork.svelte b/frontend/components/CardWork.svelte index 9edb33a..4e8cee6 100644 --- a/frontend/components/CardWork.svelte +++ b/frontend/components/CardWork.svelte @@ -1,50 +1,64 @@ - + { + if (!(event instanceof MouseEvent)) return + if (event.ctrlKey || event.metaKey) { + selected = !selected + } else { + editWork() + } + }} +> {#await thumbPath()}
{:then _} @@ -54,7 +68,7 @@ $: dispatch(selected ? "select" : "deselect", { work })

@@ -68,8 +82,12 @@ $: dispatch(selected ? "select" : "deselect", { work }) ? $_("Click to show all works") : $_("Click to only show works tagged {tag}", { values: { tag }, - })} + })} on:click|stopPropagation={e => { + if (e.ctrlKey || e.metaKey) { + selected = !selected + return + } dispatch("tag-click", tag) e.target.blur() }} diff --git a/frontend/components/ContentBlockMedia.svelte b/frontend/components/ContentBlockMedia.svelte index f56e091..c4cca03 100644 --- a/frontend/components/ContentBlockMedia.svelte +++ b/frontend/components/ContentBlockMedia.svelte @@ -1,25 +1,25 @@ -
- {#if generalContentType(block.data.source) === "image"} - - {:else if generalContentType(block.data.source) === "video"} +
+ {#if generalContentType(block.data.relativeSource) === "image"} + + {:else if generalContentType(block.data.relativeSource) === "video"}
-{#if ["video", "audio"].includes(generalContentType(block.data.source))} +{#if ["video", "audio"].includes(generalContentType(block.data.relativeSource))}
(activeBlock = block.id)} on:blur={() => (activeBlock = null)} /> - {#if generalContentType(block.data.source) === "video"} + {#if generalContentType(block.data.relativeSource) === "video"} -import gridHelp from "svelte-grid/build/helper" +import { diff } from "just-diff" +import { createEventDispatcher, onMount } from "svelte" import Grid from "svelte-grid" -import MarkdownEditor from "./MarkdownEditor.svelte" -import { scale } from "svelte/transition" -import { tooltip } from "../actions" -import { - backend, - localDatabase, - localProjects, - relativeToDatabase, -} from "../backend" +import gridHelp from "svelte-grid/build/helper" +import { _ } from "svelte-i18n" import { ContentBlock, - eachLanguage, - emptyContentUnit, - fromBlocksToParsedDescription, ItemID, + emptyContentUnit, toBlocks, } from "../contentblocks" -import { - inLanguage, - LayedOutElement, - ParsedDescription, - Translated, -} from "../ortfo" -import { - settings, - state, - workOnDisk, - workOnDiskCurrentLanguage, - debugFlyoutContent, -} from "../stores" -import { createEventDispatcher, onMount } from "svelte" -import { diff } from "just-diff" -import { _ } from "svelte-i18n" -import { i18n } from "../actions" -import FieldFilepath from "./FieldFilepath.svelte" -import FieldText from "./FieldText.svelte" -import { rebuildDatabase } from "./Navbar.svelte" -import CardContentBlock from "./CardContentBlock.svelte" -import { deleteWorks } from "../modals/ConfirmDeleteWorks.svelte" -import { layoutWidth, OrtfoMkLayout } from "../layout" -import { deepRepeat, pick } from "../utils" +import { OrtfoMkLayout, layoutWidth } from "../layout" +import { localize, type AnalyzedWork, type Translated } from "../ortfo" +import { debugFlyoutContent, settings, state, workOnDisk } from "../stores" import hotkeys from "../tinykeysInputDisabled" +import { deepRepeat, pick } from "../utils" +import CardContentBlock from "./CardContentBlock.svelte" const dispatch = createEventDispatcher() -export let work: ParsedDescription +export let work: AnalyzedWork export let language: string -let blocks: Translated = {} +let blocks: Translated = Object.fromEntries( + Object.entries(work.content).map(([lang, content]) => [lang, []]), +) let cols: number[][] = [] let rowCapacity: number = 0 let rowHeight: number = 500 let _newRowCapacity = rowCapacity -let activeBlock: number | null = null +let activeBlock: string | null = null let initialized = false let error: Error | null = null @@ -62,9 +37,12 @@ onMount(async () => { try { ;[blocks, rowCapacity] = await toBlocks( work, - $settings.portfoliolanguages + $settings.portfoliolanguages, ) + console.log(`blocks:`, blocks) + console.log(`rowCapacity:`, rowCapacity) cols = [[400, rowCapacity]] + console.log("cols", cols) // Need timeout because of the scale transition setTimeout(() => { window.scrollTo({ @@ -92,7 +70,7 @@ const stretchLayout = (oldLayout: OrtfoMkLayout, oldRowCapacity, target) => { const rate = target / row.length if (!Number.isInteger(rate)) { throw Error( - `Cannot stretch layout row of size ${row.length} into size ${target} (rate is ${rate}, non integer)` + `Cannot stretch layout row of size ${row.length} into size ${target} (rate is ${rate}, non integer)`, ) } @@ -105,25 +83,34 @@ const updateRowCapacity = async (newValue: number) => { ;[blocks, rowCapacity] = await toBlocks( { ...work, - metadata: { - ...work.metadata, - layout: stretchLayout(work.metadata.layout, oldValue, newValue), - }, + content: Object.fromEntries( + Object.entries(work.content).map(([lang, content]) => [ + lang, + { + ...content, + layout: stretchLayout( + content.layout as OrtfoMkLayout, + oldValue, + newValue, + ), + }, + ]), + ), }, - $settings.portfoliolanguages + $settings.portfoliolanguages, ) cols = [[400, rowCapacity]] } const addBlock = ( - type: LayedOutElement["type"], + type: "paragraph" | "media" | "link", overrideGeometry: { x?: number | null y?: number | null h?: number | null w?: number | null - } = {} + } = {}, ) => e => { Object.entries(blocks).forEach(([lang, blocksOneLang]) => { @@ -132,17 +119,21 @@ const addBlock = x: Object.hasOwn(overrideGeometry, "x") ? overrideGeometry?.x : empty - ? 0 - : Math.min( - ...blocksOneLang.map(block => block[rowCapacity].x) - ), + ? 0 + : Math.min( + ...blocksOneLang.map( + block => block[rowCapacity].x, + ), + ), y: Object.hasOwn(overrideGeometry, "y") ? overrideGeometry?.y : empty - ? 0 - : Math.max( - ...blocksOneLang.map(block => block[rowCapacity].y) - ) + 1, + ? 0 + : Math.max( + ...blocksOneLang.map( + block => block[rowCapacity].y, + ), + ) + 1, w: overrideGeometry?.w || rowCapacity, h: overrideGeometry?.h || 1, } @@ -163,7 +154,7 @@ const addBlock = ...blocksOneLang .map(b => b.id.split(":")) .filter(([bType, _]) => bType === type) - .map(([_, id]) => parseInt(id)) + .map(([_, id]) => parseInt(id)), ) + 1 }` as ItemID @@ -192,11 +183,11 @@ const addBlock = const removeBlock = (item: ContentBlock) => e => { // updateOtherLanguages will not delete blocks that are in one language's layout but not the other, as they could've also been added from the other to the current one. // we delete the block from other languages right there. - + const itemWasAloneOnRow = Object.values(blocks).every( oneLangBlocks => oneLangBlocks.filter(b => b[rowCapacity].y === item[rowCapacity].y) - .length === 1 + .length === 1, ) blocks[$state.lang] = blocks[$state.lang] // Remove the item @@ -227,13 +218,17 @@ function updateOtherLanguages() { } } -function updateWork(blocks) { - let updatedWork = fromBlocksToParsedDescription( - blocks, - rowCapacity, - work, - language - ) +function updateWork(blocks: Translated) { + let updatedWork: AnalyzedWork = { + ...work, + content: { + ...work.content, + [language]: { + ...work.content[language], + blocks: blocks[language].map(b => b.data), + }, + }, + } let delta = diff(updatedWork, work) if (delta.length > 0) { console.info("Work changed, delta is", delta) @@ -260,26 +255,28 @@ hotkeys(window, { "$mod+P": addBlock("paragraph"), }) -$: updateWork(blocks) -$: blocks = Object.fromEntries( - Object.entries(blocks).map(([lang, blocksOneLang]) => { - console.log(`sorting blocks`) - return [ - lang, - blocksOneLang.sort((a, b) => - a[rowCapacity].y > b[rowCapacity].y || - (a[rowCapacity].y === b[rowCapacity].y && - a[rowCapacity].x > b[rowCapacity].x) - ? 1 - : -1 - ), - ] - }) -) +$: if (initialized) updateWork(blocks) +/* +$: if (blocks) + blocks = Object.fromEntries( + Object.entries(blocks).map(([lang, blocksOneLang]) => { + console.log(`sorting blocks`) + return [ + lang, + blocksOneLang.sort((a, b) => + a[rowCapacity].y > b[rowCapacity].y || + (a[rowCapacity].y === b[rowCapacity].y && + a[rowCapacity].x > b[rowCapacity].x) + ? 1 + : -1, + ), + ] + }), + )*/ $: console.log("rowHeight=", rowHeight) -$: $debugFlyoutContent = blocks[$state.lang] - ?.map(b => ({ id: b.id, ...pick(b["2"], "x", "y", "w", "h") })) - .map(b => `${b.id} (${b.x} ${b.y}) → (${b.x + b.w - 1} ${b.y + b.h - 1})`) +// $debugFlyoutContent = blocks[$state.lang] +// ?.map(b => ({ id: b.id, ...pick(b["2"], "x", "y", "w", "h") })) +// .map(b => `${b.id} (${b.x} ${b.y}) → (${b.x + b.w - 1} ${b.y + b.h - 1})`) {#if !initialized} @@ -287,7 +284,7 @@ $: $debugFlyoutContent = blocks[$state.lang] {:else if error}

{$_("An error occured: ")}

    - {#each error.toString().split(": ") as reason} + {#each (error?.toString() ?? "Unknown error").split(": ") as reason}
  • {#if Array.from(reason).includes("\n")}
    {reason}
    @@ -321,7 +318,7 @@ $: $debugFlyoutContent = blocks[$state.lang] movePointerDown(e.detail)} on:resizePointerDown={e => resizePointerDown(e.detail)} on:remove={removeBlock(item)} @@ -447,7 +444,9 @@ h2 { height: 6em; padding: 0.5em; margin: 0.5em; - transition: all 0.25s ease, font-varation-settings 1s ease, + transition: + all 0.25s ease, + font-varation-settings 1s ease, box-shadow 0.25s ease 0.125s; outline: none; border: 1px solid transparent; diff --git a/frontend/components/CreateExternalSite.svelte b/frontend/components/CreateExternalSite.svelte index eba2d30..d6dd890 100644 --- a/frontend/components/CreateExternalSite.svelte +++ b/frontend/components/CreateExternalSite.svelte @@ -9,11 +9,10 @@ import FieldText from "./FieldText.svelte" import MetadataField from "./MetadataField.svelte" const EMPTY_SITE: ExternalSite = { - aliases: [], - description: "", - learnmoreurl: "", - plural: "", - singular: "", + purpose: "", + url: "", + name: "", + username: "", } let newSite: ExternalSite = EMPTY_SITE diff --git a/frontend/components/FieldColor.svelte b/frontend/components/FieldColor.svelte index 5bd0c07..c1de20c 100644 --- a/frontend/components/FieldColor.svelte +++ b/frontend/components/FieldColor.svelte @@ -1,9 +1,11 @@ - + class:unset={!value} + on:click={event => { + if ( + event instanceof MouseEvent && + (event.ctrlKey || event.metaKey) + ) { + event.preventDefault() + event.stopPropagation() + value = "" + } + }} + class="swatch" + > + + {value || $_("unspecified")} diff --git a/frontend/components/FieldColors.svelte b/frontend/components/FieldColors.svelte index 13df65b..4195625 100644 --- a/frontend/components/FieldColors.svelte +++ b/frontend/components/FieldColors.svelte @@ -1,14 +1,13 @@ -