diff --git a/langs/en.json b/langs/en.json index e68ab5de7..d3c0f0dd4 100644 --- a/langs/en.json +++ b/langs/en.json @@ -393,6 +393,9 @@ "search": { "error": "Something went wrong…", "nothing": "Nothing found…", + "nothingFor": "No results found for {term}", + "recentThemes": "Recently visited maps", + "recents": "Recent searches", "search": "Search a location", "searchShort": "Search…", "searching": "Searching…" diff --git a/public/css/index-tailwind-output.css b/public/css/index-tailwind-output.css index 04137545c..506dc50b3 100644 --- a/public/css/index-tailwind-output.css +++ b/public/css/index-tailwind-output.css @@ -2558,11 +2558,6 @@ video { border-color: rgb(209 213 219 / var(--tw-border-opacity)); } -.border-red-500 { - --tw-border-opacity: 1; - border-color: rgb(239 68 68 / var(--tw-border-opacity)); -} - .border-gray-800 { --tw-border-opacity: 1; border-color: rgb(31 41 55 / var(--tw-border-opacity)); @@ -2658,6 +2653,11 @@ video { border-color: rgb(34 197 94 / var(--tw-border-opacity)); } +.border-red-500 { + --tw-border-opacity: 1; + border-color: rgb(239 68 68 / var(--tw-border-opacity)); +} + .border-gray-700 { --tw-border-opacity: 1; border-color: rgb(55 65 81 / var(--tw-border-opacity)); @@ -4379,10 +4379,10 @@ video { } :root { - /* - * The main colour scheme of mapcomplete is configured here. - * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. - */ + /* + * The main colour scheme of mapcomplete is configured here. + * For a custom styling, set 'customCss' in your layoutConfig and overwrite some of these. + */ /* No support for dark mode yet, we disable it to prevent some elements to suddenly toggle */ color-scheme: only light; /* Main color of the application: the background and text colours */ @@ -4407,9 +4407,9 @@ video { --disabled: #B8B8B8; --disabled-font: #B8B8B8; /** - * Base colour of interactive elements, mainly the 'subtle button' - * @deprecated - */ + * Base colour of interactive elements, mainly the 'subtle button' + * @deprecated + */ --subtle-detail-color: #dbeafe; --subtle-detail-color-contrast: black; --subtle-detail-color-light-contrast: lightgrey; @@ -4419,14 +4419,14 @@ video { --catch-detail-color-contrast: #fb3afb; --image-carousel-height: 350px; /** Technical value, used by icon.svelte - */ + */ --svg-color: #000000; } -@font-face{ - font-family:"Source Sans Pro"; +@font-face { + font-family: "Source Sans Pro"; - src:url("/assets/source-sans-pro.regular.ttf") format("woff"); + src: url("/assets/source-sans-pro.regular.ttf") format("woff"); } /***********************************************************************\ @@ -4663,18 +4663,18 @@ select:hover { .neutral-label { /** This label styles as normal text. It's power comes from the many :not(.neutral-label) entries. - * Placed here for autocompletion - */ + * Placed here for autocompletion + */ } label:not(.neutral-label):not(.button) { /** - * Label should _contain_ the input element - */ + * Label should _contain_ the input element + */ padding: 0.25rem; padding-right: 0.5rem; padding-left: 0.5rem; - margin:0.25rem; + margin: 0.25rem; border-radius: 0.5rem; width: 100%; box-sizing: border-box; @@ -4887,6 +4887,10 @@ a.link-underline { color: unset !important; } +a:hover { + background-color: var(--low-interaction-background); +} + .disable-links a.must-link, .disable-links .must-link a { /* Hide links if they are disabled */ @@ -4901,7 +4905,7 @@ a.link-underline { .selected svg:not(.noselect *) path.selectable { /* A marker on the map gets the 'selected' class when it's properties are displayed - */ + */ stroke: white !important; stroke-width: 20px !important; overflow: visible !important; @@ -4915,7 +4919,7 @@ a.link-underline { .selected svg { /* A marker on the map gets the 'selected' class when it's properties are displayed - */ + */ overflow: visible !important; } diff --git a/src/Logic/Geocoding/CombinedSearcher.ts b/src/Logic/Geocoding/CombinedSearcher.ts index c26e445d3..e2136e7fb 100644 --- a/src/Logic/Geocoding/CombinedSearcher.ts +++ b/src/Logic/Geocoding/CombinedSearcher.ts @@ -1,12 +1,13 @@ import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" +import { Utils } from "../../Utils" export default class CombinedSearcher implements GeocodingProvider { private _providers: ReadonlyArray private _providersWithSuggest: ReadonlyArray constructor(...providers: ReadonlyArray) { - this._providers = providers - this._providersWithSuggest = providers.filter(pr => pr.suggest !== undefined) + this._providers = Utils.NoNull(providers) + this._providersWithSuggest = this._providers.filter(pr => pr.suggest !== undefined) } /** diff --git a/src/Logic/Geocoding/GeocodingProvider.ts b/src/Logic/Geocoding/GeocodingProvider.ts index a174ef88c..16ef72648 100644 --- a/src/Logic/Geocoding/GeocodingProvider.ts +++ b/src/Logic/Geocoding/GeocodingProvider.ts @@ -24,7 +24,7 @@ export type GeoCodeResult = { osm_type?: "node" | "way" | "relation" osm_id?: string, category?: GeocodingCategory, - importance?: number + payload?: object } export interface GeocodingOptions { diff --git a/src/Logic/Geocoding/RecentSearch.ts b/src/Logic/Geocoding/RecentSearch.ts index 5ba22bca3..070ac7b34 100644 --- a/src/Logic/Geocoding/RecentSearch.ts +++ b/src/Logic/Geocoding/RecentSearch.ts @@ -7,21 +7,20 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig" export class RecentSearch { - private readonly _recentSearches: UIEventSource - public readonly recentSearches: Store - private readonly _seenThisSession: UIEventSource = new UIEventSource([]) - public readonly seenThisSession: Store = this._seenThisSession + private readonly _seenThisSession: UIEventSource + public readonly seenThisSession: Store constructor(state: { layout: LayoutConfig, osmConnection: OsmConnection, selectedElement: Store }) { - const longPref = state.osmConnection.preferencesHandler.GetLongPreference("recent-searches") - this._recentSearches = longPref.sync(str => !str ? [] : JSON.parse(str), [], strs => JSON.stringify(strs)) - this.recentSearches = this._recentSearches + // const prefs = state.osmConnection.preferencesHandler.GetLongPreference("previous-searches") + this._seenThisSession = new UIEventSource([])//UIEventSource.asObject(prefs, []) + this.seenThisSession = this._seenThisSession + state.selectedElement.addCallbackAndRunD(selected => { const [osm_type, osm_id] = selected.properties.id.split("/") const [lon, lat] = GeoOperations.centerpointCoordinates(selected) - const entry = { + const entry = { feature: selected, osm_id, osm_type, description: "Viewed recently", @@ -33,7 +32,7 @@ export class RecentSearch { } addSelected(entry: GeoCodeResult) { - const arr = [...this.seenThisSession.data.slice(0, 20), entry] + const arr = [...(this.seenThisSession.data ?? []).slice(0, 20), entry] const seenIds = new Set() for (let i = arr.length - 1; i >= 0; i--) { diff --git a/src/Logic/Geocoding/ThemeSearch.ts b/src/Logic/Geocoding/ThemeSearch.ts new file mode 100644 index 000000000..088cae184 --- /dev/null +++ b/src/Logic/Geocoding/ThemeSearch.ts @@ -0,0 +1,43 @@ +import GeocodingProvider, { GeoCodeResult, GeocodingOptions } from "./GeocodingProvider" +import * as themeOverview from "../../assets/generated/theme_overview.json" +import { MinimalLayoutInformation } from "../../Models/ThemeConfig/LayoutConfig" +import { SpecialVisualizationState } from "../../UI/SpecialVisualization" +import { Utils } from "../../Utils" +import MoreScreen from "../../UI/BigComponents/MoreScreen" +import { Store } from "../UIEventSource" + +export default class ThemeSearch implements GeocodingProvider { + + private static allThemes: MinimalLayoutInformation[] = (themeOverview["default"] ?? themeOverview) + private readonly _state: SpecialVisualizationState + private readonly _knownHiddenThemes: Store> + + constructor(state: SpecialVisualizationState) { + this._state = state + this._knownHiddenThemes = MoreScreen.knownHiddenThemes(this._state.osmConnection) + } + + search(query: string, options?: GeocodingOptions): Promise { + return this.suggest(query, options) + } + + async suggest?(query: string, options?: GeocodingOptions): Promise { + if(query.length < 1){ + return [] + } + const limit = options?.limit ?? 4 + query = Utils.simplifyStringForSearch(query) + const withMatch = ThemeSearch.allThemes + .filter(th => !th.hideFromOverview ) + .filter(th => th.id !== this._state.layout.id) + .filter(th => MoreScreen.MatchesLayout(th, query)) + .slice(0, limit + 1) + + return withMatch.map(match => ( { + payload: match, + osm_id: match.id + })) + } + + +} diff --git a/src/Logic/Osm/OsmPreferences.ts b/src/Logic/Osm/OsmPreferences.ts index 597dd61ea..6ac95d8c8 100644 --- a/src/Logic/Osm/OsmPreferences.ts +++ b/src/Logic/Osm/OsmPreferences.ts @@ -71,7 +71,7 @@ export class OsmPreferences { } if (str === null) { console.error("Deleting " + allStartWith) - let count = parseInt(length.data) + const count = parseInt(length.data) for (let i = 0; i < count; i++) { // Delete all the preferences self.GetPreference(allStartWith + "-" + i, "", subOptions).setData("") diff --git a/src/Logic/State/UserRelatedState.ts b/src/Logic/State/UserRelatedState.ts index c11db56fe..ea59e04de 100644 --- a/src/Logic/State/UserRelatedState.ts +++ b/src/Logic/State/UserRelatedState.ts @@ -78,6 +78,10 @@ export default class UserRelatedState { public readonly preferencesAsTags: UIEventSource> private readonly _mapProperties: MapProperties + private readonly _recentlyVisitedThemes: UIEventSource + public readonly recentlyVisitedThemes: Store + + constructor( osmConnection: OsmConnection, layout?: LayoutConfig, @@ -109,7 +113,7 @@ export default class UserRelatedState { this.showAllQuestionsAtOnce = UIEventSource.asBoolean( this.osmConnection.GetPreference("show-all-questions", "false", { documentation: - "Either 'true' or 'false'. If set, all questions will be shown all at once", + "Either 'true' or 'false'. If set, all questions will be shown all at once" }) ) this.language = this.osmConnection.GetPreference("language") @@ -129,7 +133,7 @@ export default class UserRelatedState { undefined, { documentation: - "The ID of a layer or layer category that MapComplete uses by default", + "The ID of a layer or layer category that MapComplete uses by default" } ) @@ -137,12 +141,12 @@ export default class UserRelatedState { "preferences-add-new-mode", "button_click_right", { - documentation: "How adding a new feature is done", + documentation: "How adding a new feature is done" } ) this.imageLicense = this.osmConnection.GetPreference("pictures-license", "CC0", { - documentation: "The license under which new images are uploaded", + documentation: "The license under which new images are uploaded" }) this.installedUserThemes = this.InitInstalledUserThemes() @@ -150,6 +154,30 @@ export default class UserRelatedState { this.preferencesAsTags = this.initAmendedPrefs(layout, featureSwitches) + const prefs = this.osmConnection + this._recentlyVisitedThemes = UIEventSource.asObject(prefs.GetLongPreference("recently-visited-themes"), []) + this.recentlyVisitedThemes = this._recentlyVisitedThemes + if (layout) { + const osmConn =this.osmConnection + const recentlyVisited = this._recentlyVisitedThemes + function update() { + if (!osmConn.isLoggedIn.data) { + return + } + const previously = recentlyVisited.data + if (previously[0] === layout.id) { + return true + } + const newThemes = Utils.Dedup([layout.id, ...previously]).slice(0, 30) + recentlyVisited.set(newThemes) + return true + } + + + this._recentlyVisitedThemes.addCallbackAndRun(() => update()) + this.osmConnection.isLoggedIn.addCallbackAndRun(() => update()) + } + this.syncLanguage() } @@ -171,13 +199,13 @@ export default class UserRelatedState { public GetUnofficialTheme(id: string): | { - id: string - icon: string - title: any - shortDescription: any - definition?: any - isOfficial: boolean - } + id: string + icon: string + title: any + shortDescription: any + definition?: any + isOfficial: boolean + } | undefined { console.log("GETTING UNOFFICIAL THEME") const pref = this.osmConnection.GetLongPreference("unofficial-theme-" + id) @@ -202,8 +230,8 @@ export default class UserRelatedState { } catch (e) { console.warn( "Removing theme " + - id + - " as it could not be parsed from the preferences; the content is:", + id + + " as it could not be parsed from the preferences; the content is:", str ) pref.setData(null) @@ -233,7 +261,7 @@ export default class UserRelatedState { icon: layout.icon, title: layout.title.translations, shortDescription: layout.shortDescription.translations, - definition: layout["definition"], + definition: layout["definition"] }) ) } @@ -273,13 +301,13 @@ export default class UserRelatedState { id: "home", "user:home": "yes", _lon: homeLonLat[0], - _lat: homeLonLat[1], + _lat: homeLonLat[1] }, geometry: { type: "Point", - coordinates: homeLonLat, - }, - }, + coordinates: homeLonLat + } + } ] }) return new StaticFeatureSource(feature) @@ -300,7 +328,7 @@ export default class UserRelatedState { _applicationOpened: new Date().toISOString(), _supports_sharing: typeof window === "undefined" ? "no" : window.navigator.share ? "yes" : "no", - _iframe: Utils.isIframe ? "yes" : "no", + _iframe: Utils.isIframe ? "yes" : "no" }) for (const key in Constants.userJourney) { @@ -355,18 +383,18 @@ export default class UserRelatedState { const zenLinks: { link: string; id: string }[] = Utils.NoNull([ hasMissingTheme ? { - id: "theme:" + layout.id, - link: LinkToWeblate.hrefToWeblateZen( - language, - "themes", - layout.id - ), - } + id: "theme:" + layout.id, + link: LinkToWeblate.hrefToWeblateZen( + language, + "themes", + layout.id + ) + } : undefined, ...missingLayers.map((id) => ({ id: "layer:" + id, - link: LinkToWeblate.hrefToWeblateZen(language, "layers", id), - })), + link: LinkToWeblate.hrefToWeblateZen(language, "layers", id) + })) ]) const untranslated_count = untranslated.length amendedPrefs.data["_translation_total"] = "" + total @@ -391,8 +419,8 @@ export default class UserRelatedState { for (const k in userDetails) { amendedPrefs.data["_" + k] = "" + userDetails[k] } - if(userDetails.description){ - amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter() + if (userDetails.description) { + amendedPrefs.data["_description_html"] = Utils.purify(new Showdown.Converter() .makeHtml(userDetails.description) ?.replace(/>/g, ">") ?.replace(/</g, "<") diff --git a/src/Logic/UIEventSource.ts b/src/Logic/UIEventSource.ts index 4fc839405..cb2cfc773 100644 --- a/src/Logic/UIEventSource.ts +++ b/src/Logic/UIEventSource.ts @@ -104,7 +104,9 @@ export abstract class Store implements Readable { extraStoresToWatch: Store[], callbackDestroyFunction: (f: () => void) => void ): Store + M + public mapD( f: (t: Exclude) => J, extraStoresToWatch?: Store[], @@ -246,6 +248,7 @@ export abstract class Store implements Readable { return f(>t) }) } + public stabilized(millisToStabilize): Store { if (Utils.runningFromConsole) { return this @@ -311,12 +314,14 @@ export class ImmutableStore extends Store { public readonly data: T static FALSE = new ImmutableStore(false) static TRUE = new ImmutableStore(true) + constructor(data: T) { super() this.data = data } - private static readonly pass: () => void = () => {} + private static readonly pass: () => void = () => { + } addCallback(_: (data: T) => void): () => void { // pass: data will never change @@ -718,6 +723,27 @@ export class UIEventSource extends Store implements Writable { ) } + static asObject(stringUIEventSource: UIEventSource, defaultV: T): UIEventSource { + return stringUIEventSource.sync( + (str) => { + if (str === undefined || str === null || str === "") { + return defaultV + } + try { + return JSON.parse(str) + } catch (e) { + console.error("Could not parse value", str,"due to",e) + return defaultV + } + }, + [], + (b) => { + console.log("Stringifying", b) + return JSON.stringify(b) ?? "" + } + ) + } + /** * Create a new UIEVentSource. Whenever 'source' changes, the returned UIEventSource will get this value as well. * However, this value can be overriden without affecting source @@ -863,7 +889,7 @@ export class UIEventSource extends Store implements Writable { const newSource = new UIEventSource(f(this.data), "map(" + this.tag + ")@" + callee) - const update = function () { + const update = function() { newSource.setData(f(self.data)) return allowUnregister && newSource._callbacks.length() === 0 } diff --git a/src/Models/ThemeConfig/LayoutConfig.ts b/src/Models/ThemeConfig/LayoutConfig.ts index 3863938fb..2f20e1fdb 100644 --- a/src/Models/ThemeConfig/LayoutConfig.ts +++ b/src/Models/ThemeConfig/LayoutConfig.ts @@ -12,7 +12,21 @@ import { RasterLayerProperties } from "../RasterLayerProperties" import { ConversionContext } from "./Conversion/ConversionContext" import { Translatable } from "./Json/Translatable" +import { MinimalTagRenderingConfigJson, TagRenderingConfigJson } from "./Json/TagRenderingConfigJson" +/** + * Minimal information about a theme + **/ +export class MinimalLayoutInformation { + id: string + icon: string + title: Translatable + shortDescription: Translatable + definition?: Translatable + mustHaveLanguage?: boolean + hideFromOverview?: boolean + keywords?: (Translatable | TagRenderingConfigJson)[] +} /** * Minimal information about a theme **/ @@ -27,6 +41,8 @@ export class LayoutInformation { keywords?: (Translatable | Translation)[] } + + export default class LayoutConfig implements LayoutInformation { public static readonly defaultSocialImage = "assets/SocialImage.png" public readonly id: string diff --git a/src/Models/ThemeViewState.ts b/src/Models/ThemeViewState.ts index a9d9bfd0c..132ec6e03 100644 --- a/src/Models/ThemeViewState.ts +++ b/src/Models/ThemeViewState.ts @@ -81,6 +81,7 @@ import CoordinateSearch from "../Logic/Geocoding/CoordinateSearch" import LocalElementSearch from "../Logic/Geocoding/LocalElementSearch" import { RecentSearch } from "../Logic/Geocoding/RecentSearch" import PhotonSearch from "../Logic/Geocoding/PhotonSearch" +import ThemeSearch from "../Logic/Geocoding/ThemeSearch" /** * @@ -393,6 +394,7 @@ export default class ThemeViewState implements SpecialVisualizationState { new LocalElementSearch(this, 5), new PhotonSearch(), // new NominatimGeocoding(), new CoordinateSearch(), + this.featureSwitches.featureSwitchBackToThemeOverview.data ? new ThemeSearch(this) : undefined ) this.recentlySearched = new RecentSearch(this) diff --git a/src/UI/AllThemesGui.svelte b/src/UI/AllThemesGui.svelte index fc2977eb7..622c41bf6 100644 --- a/src/UI/AllThemesGui.svelte +++ b/src/UI/AllThemesGui.svelte @@ -35,7 +35,7 @@ "oauth_token", undefined, "Used to complete the login" - ), + ) }) const state = new UserRelatedState(osmConnection) const t = Translations.t.index @@ -44,7 +44,7 @@ let userLanguages = osmConnection.userDetails.map((ud) => ud.languages) let themeSearchText: UIEventSource = new UIEventSource(undefined) - document.addEventListener("keydown", function (event) { + document.addEventListener("keydown", function(event) { if (event.ctrlKey && event.code === "KeyF") { document.getElementById("theme-search")?.focus() event.preventDefault() @@ -55,20 +55,10 @@ const hiddenThemes: LayoutInformation[] = (themeOverview["default"] ?? themeOverview)?.filter((layout) => layout.hideFromOverview) ?? [] { - const prefix = "mapcomplete-hidden-theme-" - const userPreferences = state.osmConnection.preferencesHandler.preferences - visitedHiddenThemes = userPreferences.map((preferences) => { - const knownIds = new Set( - Object.keys(preferences) - .filter((key) => key.startsWith(prefix)) - .map((key) => key.substring(prefix.length, key.length - "-enabled".length)) - ) - return hiddenThemes.filter( - (theme) => - knownIds.has(theme.id) || - state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" - ) - }) + visitedHiddenThemes = MoreScreen.knownHiddenThemes(state.osmConnection) + .map((knownIds) => hiddenThemes.filter((theme) => + knownIds.has(theme.id) || state.osmConnection.userDetails.data.name === "Pieter Vander Vennet" + )) } @@ -103,7 +93,7 @@
MoreScreen.applySearch(themeSearchText.data)} + on:submit|preventDefault={() => MoreScreen.applySearch(themeSearchText.data)} >