forked from MapComplete/MapComplete
Refactoring: move all code files into a src directory
This commit is contained in:
parent
de99f56ca8
commit
e75d2789d2
389 changed files with 0 additions and 12 deletions
29
src/UI/Image/AttributedImage.ts
Normal file
29
src/UI/Image/AttributedImage.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Attribution from "./Attribution"
|
||||
import Img from "../Base/Img"
|
||||
import ImageProvider from "../../Logic/ImageProviders/ImageProvider"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Mapillary } from "../../Logic/ImageProviders/Mapillary"
|
||||
import { UIEventSource } from "../../Logic/UIEventSource"
|
||||
|
||||
export class AttributedImage extends Combine {
|
||||
constructor(imageInfo: { url: string; provider?: ImageProvider; date?: Date }) {
|
||||
let img: BaseUIElement
|
||||
img = new Img(imageInfo.url, false, {
|
||||
fallbackImage:
|
||||
imageInfo.provider === Mapillary.singleton ? "./assets/svg/blocked.svg" : undefined,
|
||||
})
|
||||
|
||||
let attr: BaseUIElement = undefined
|
||||
if (imageInfo.provider !== undefined) {
|
||||
attr = new Attribution(
|
||||
UIEventSource.FromPromise(imageInfo.provider?.DownloadAttribution(imageInfo.url)),
|
||||
imageInfo.provider?.SourceIcon(),
|
||||
imageInfo.date
|
||||
)
|
||||
}
|
||||
|
||||
super([img, attr])
|
||||
this.SetClass("block relative h-full")
|
||||
}
|
||||
}
|
||||
50
src/UI/Image/Attribution.ts
Normal file
50
src/UI/Image/Attribution.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import { LicenseInfo } from "../../Logic/ImageProviders/LicenseInfo"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import Link from "../Base/Link"
|
||||
|
||||
/**
|
||||
* Small box in the bottom left of an image, e.g. the image in a popup
|
||||
*/
|
||||
export default class Attribution extends VariableUiElement {
|
||||
constructor(license: Store<LicenseInfo>, icon: BaseUIElement, date?: Date) {
|
||||
if (license === undefined) {
|
||||
throw "No license source given in the attribution element"
|
||||
}
|
||||
super(
|
||||
license.map((license: LicenseInfo) => {
|
||||
if (license === undefined) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
let title = undefined
|
||||
if (license?.title) {
|
||||
title = Translations.W(license?.title).SetClass("block")
|
||||
if (license.informationLocation) {
|
||||
title = new Link(title, license.informationLocation.href, true)
|
||||
}
|
||||
}
|
||||
return new Combine([
|
||||
icon
|
||||
?.SetClass("block left")
|
||||
.SetStyle("height: 2em; width: 2em; padding-right: 0.5em;"),
|
||||
|
||||
new Combine([
|
||||
title,
|
||||
Translations.W(license?.artist ?? "").SetClass("block font-bold"),
|
||||
Translations.W(license?.license ?? license?.licenseShortName),
|
||||
date === undefined
|
||||
? undefined
|
||||
: new FixedUiElement(date.toLocaleDateString()),
|
||||
]).SetClass("flex flex-col"),
|
||||
]).SetClass(
|
||||
"flex flex-row bg-black text-white text-sm absolute bottom-0 left-0 p-0.5 pl-5 pr-3 rounded-lg no-images"
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
71
src/UI/Image/DeleteImage.ts
Normal file
71
src/UI/Image/DeleteImage.ts
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Toggle, { ClickableToggle } from "../Input/Toggle"
|
||||
import Combine from "../Base/Combine"
|
||||
import Svg from "../../Svg"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
export default class DeleteImage extends Toggle {
|
||||
constructor(
|
||||
key: string,
|
||||
tags: Store<any>,
|
||||
state: { layout: LayoutConfig; changes?: Changes; osmConnection?: OsmConnection }
|
||||
) {
|
||||
const oldValue = tags.data[key]
|
||||
const isDeletedBadge = Translations.t.image.isDeleted
|
||||
.Clone()
|
||||
.SetClass("rounded-full p-1")
|
||||
.SetStyle("color:white;background:#ff8c8c")
|
||||
.onClick(async () => {
|
||||
await state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data, {
|
||||
changeType: "delete-image",
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const deleteButton = Translations.t.image.doDelete
|
||||
.Clone()
|
||||
.SetClass("block w-full pl-4 pr-4")
|
||||
.SetStyle(
|
||||
"color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;"
|
||||
)
|
||||
.onClick(async () => {
|
||||
await state?.changes?.applyAction(
|
||||
new ChangeTagAction(tags.data.id, new Tag(key, ""), tags.data, {
|
||||
changeType: "answer",
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
const cancelButton = Translations.t.general.cancel
|
||||
.Clone()
|
||||
.SetClass("bg-white pl-4 pr-4")
|
||||
.SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;")
|
||||
const openDelete = Svg.delete_icon_svg().SetStyle("width: 2em; height: 2em; display:block;")
|
||||
const deleteDialog = new ClickableToggle(
|
||||
new Combine([deleteButton, cancelButton]).SetClass("flex flex-col background-black"),
|
||||
openDelete
|
||||
)
|
||||
|
||||
cancelButton.onClick(() => deleteDialog.isEnabled.setData(false))
|
||||
openDelete.onClick(() => deleteDialog.isEnabled.setData(true))
|
||||
|
||||
super(
|
||||
new Toggle(
|
||||
deleteDialog,
|
||||
isDeletedBadge,
|
||||
tags.map((tags) => (tags[key] ?? "") !== "")
|
||||
),
|
||||
undefined /*Login (and thus editing) is disabled*/,
|
||||
state?.osmConnection?.isLoggedIn
|
||||
)
|
||||
this.SetClass("cursor-pointer")
|
||||
}
|
||||
}
|
||||
53
src/UI/Image/ImageCarousel.ts
Normal file
53
src/UI/Image/ImageCarousel.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { SlideShow } from "./SlideShow"
|
||||
import { Store } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import DeleteImage from "./DeleteImage"
|
||||
import { AttributedImage } from "./AttributedImage"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import ImageProvider from "../../Logic/ImageProviders/ImageProvider"
|
||||
import { OsmConnection } from "../../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../../Logic/Osm/Changes"
|
||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"
|
||||
|
||||
export class ImageCarousel extends Toggle {
|
||||
constructor(
|
||||
images: Store<{ key: string; url: string; provider: ImageProvider }[]>,
|
||||
tags: Store<any>,
|
||||
state: { osmConnection?: OsmConnection; changes?: Changes; layout: LayoutConfig }
|
||||
) {
|
||||
const uiElements = images.map(
|
||||
(imageURLS: { key: string; url: string; provider: ImageProvider }[]) => {
|
||||
const uiElements: BaseUIElement[] = []
|
||||
for (const url of imageURLS) {
|
||||
try {
|
||||
let image = new AttributedImage(url)
|
||||
|
||||
if (url.key !== undefined) {
|
||||
image = new Combine([
|
||||
image,
|
||||
new DeleteImage(url.key, tags, state).SetClass(
|
||||
"delete-image-marker absolute top-0 left-0 pl-3"
|
||||
),
|
||||
]).SetClass("relative")
|
||||
}
|
||||
image
|
||||
.SetClass("w-full block")
|
||||
.SetStyle("min-width: 50px; background: grey;")
|
||||
uiElements.push(image)
|
||||
} catch (e) {
|
||||
console.error("Could not generate image element for", url.url, "due to", e)
|
||||
}
|
||||
}
|
||||
return uiElements
|
||||
}
|
||||
)
|
||||
|
||||
super(
|
||||
new SlideShow(uiElements).SetClass("w-full"),
|
||||
undefined,
|
||||
uiElements.map((els) => els.length > 0)
|
||||
)
|
||||
this.SetClass("block w-full")
|
||||
}
|
||||
}
|
||||
202
src/UI/Image/ImageUploadFlow.ts
Normal file
202
src/UI/Image/ImageUploadFlow.ts
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
import { Store, UIEventSource } from "../../Logic/UIEventSource"
|
||||
import Combine from "../Base/Combine"
|
||||
import Translations from "../i18n/Translations"
|
||||
import Svg from "../../Svg"
|
||||
import { Tag } from "../../Logic/Tags/Tag"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import Toggle from "../Input/Toggle"
|
||||
import FileSelectorButton from "../Input/FileSelectorButton"
|
||||
import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"
|
||||
import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"
|
||||
import LayerConfig from "../../Models/ThemeConfig/LayerConfig"
|
||||
import { FixedUiElement } from "../Base/FixedUiElement"
|
||||
import { VariableUiElement } from "../Base/VariableUIElement"
|
||||
import Loading from "../Base/Loading"
|
||||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||
|
||||
constructor(
|
||||
tagsSource: Store<any>,
|
||||
state: SpecialVisualizationState,
|
||||
imagePrefix: string = "image",
|
||||
text: string = undefined
|
||||
) {
|
||||
const perId = ImageUploadFlow.uploadCountsPerId
|
||||
const id = tagsSource.data.id
|
||||
if (!perId.has(id)) {
|
||||
perId.set(id, new UIEventSource<number>(0))
|
||||
}
|
||||
const uploadedCount = perId.get(id)
|
||||
const uploader = new ImgurUploader(async (url) => {
|
||||
// A file was uploaded - we add it to the tags of the object
|
||||
|
||||
const tags = tagsSource.data
|
||||
let key = imagePrefix
|
||||
if (tags[imagePrefix] !== undefined) {
|
||||
let freeIndex = 0
|
||||
while (tags[imagePrefix + ":" + freeIndex] !== undefined) {
|
||||
freeIndex++
|
||||
}
|
||||
key = imagePrefix + ":" + freeIndex
|
||||
}
|
||||
|
||||
await state.changes.applyAction(
|
||||
new ChangeTagAction(tags.id, new Tag(key, url), tagsSource.data, {
|
||||
changeType: "add-image",
|
||||
theme: state.layout.id,
|
||||
})
|
||||
)
|
||||
console.log("Adding image:" + key, url)
|
||||
uploadedCount.data++
|
||||
uploadedCount.ping()
|
||||
})
|
||||
|
||||
const t = Translations.t.image
|
||||
|
||||
let labelContent: BaseUIElement
|
||||
if (text === undefined) {
|
||||
labelContent = Translations.t.image.addPicture
|
||||
.Clone()
|
||||
.SetClass("block align-middle mt-1 ml-3 text-4xl ")
|
||||
} else {
|
||||
labelContent = new FixedUiElement(text).SetClass(
|
||||
"block align-middle mt-1 ml-3 text-2xl "
|
||||
)
|
||||
}
|
||||
const label = new Combine([
|
||||
Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl "),
|
||||
labelContent,
|
||||
]).SetClass("w-full flex justify-center items-center")
|
||||
|
||||
const licenseStore = state?.osmConnection?.GetPreference(
|
||||
Constants.OsmPreferenceKeyPicturesLicense,
|
||||
"CC0"
|
||||
)
|
||||
|
||||
const fileSelector = new FileSelectorButton(label, {
|
||||
acceptType: "image/*",
|
||||
allowMultiple: true,
|
||||
labelClasses: "rounded-full border-2 border-black font-bold",
|
||||
})
|
||||
/* fileSelector.SetClass(
|
||||
"p-2 border-4 border-detail rounded-full font-bold h-full align-middle w-full flex justify-center"
|
||||
)
|
||||
.SetStyle(" border-color: var(--foreground-color);")*/
|
||||
fileSelector.GetValue().addCallback((filelist) => {
|
||||
if (filelist === undefined || filelist.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
for (var i = 0; i < filelist.length; i++) {
|
||||
const sizeInBytes = filelist[i].size
|
||||
console.log(filelist[i].name + " has a size of " + sizeInBytes + " Bytes")
|
||||
if (sizeInBytes > uploader.maxFileSizeInMegabytes * 1000000) {
|
||||
alert(
|
||||
Translations.t.image.toBig.Subs({
|
||||
actual_size: Math.floor(sizeInBytes / 1000000) + "MB",
|
||||
max_size: uploader.maxFileSizeInMegabytes + "MB",
|
||||
}).txt
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const license = licenseStore?.data ?? "CC0"
|
||||
|
||||
const tags = tagsSource.data
|
||||
|
||||
const layout = state?.layout
|
||||
let matchingLayer: LayerConfig = undefined
|
||||
for (const layer of layout?.layers ?? []) {
|
||||
if (layer.source.osmTags.matchesProperties(tags)) {
|
||||
matchingLayer = layer
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const title =
|
||||
matchingLayer?.title?.GetRenderValue(tags)?.Subs(tags)?.ConstructElement()
|
||||
?.textContent ??
|
||||
tags.name ??
|
||||
"https//osm.org/" + tags.id
|
||||
const description = [
|
||||
"author:" + state.osmConnection.userDetails.data.name,
|
||||
"license:" + license,
|
||||
"osmid:" + tags.id,
|
||||
].join("\n")
|
||||
|
||||
uploader.uploadMany(title, description, filelist)
|
||||
})
|
||||
|
||||
const uploadFlow: BaseUIElement = new Combine([
|
||||
new VariableUiElement(
|
||||
uploader.queue
|
||||
.map((q) => q.length)
|
||||
.map((l) => {
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
if (l == 1) {
|
||||
return new Loading(t.uploadingPicture).SetClass("alert")
|
||||
} else {
|
||||
return new Loading(
|
||||
t.uploadingMultiple.Subs({ count: "" + l })
|
||||
).SetClass("alert")
|
||||
}
|
||||
})
|
||||
),
|
||||
new VariableUiElement(
|
||||
uploader.failed
|
||||
.map((q) => q.length)
|
||||
.map((l) => {
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
console.log(l)
|
||||
return t.uploadFailed.SetClass("block alert")
|
||||
})
|
||||
),
|
||||
new VariableUiElement(
|
||||
uploadedCount.map((l) => {
|
||||
if (l == 0) {
|
||||
return undefined
|
||||
}
|
||||
if (l == 1) {
|
||||
return t.uploadDone.Clone().SetClass("thanks block")
|
||||
}
|
||||
return t.uploadMultipleDone.Subs({ count: l }).SetClass("thanks block")
|
||||
})
|
||||
),
|
||||
|
||||
fileSelector,
|
||||
new Combine([
|
||||
Translations.t.image.respectPrivacy,
|
||||
new VariableUiElement(
|
||||
licenseStore.map((license) =>
|
||||
Translations.t.image.currentLicense.Subs({ license })
|
||||
)
|
||||
)
|
||||
.onClick(() => {
|
||||
console.log("Opening the license settings... ")
|
||||
state.guistate.openUsersettings("picture-license")
|
||||
})
|
||||
.SetClass("underline"),
|
||||
]).SetStyle("font-size:small;"),
|
||||
]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none")
|
||||
|
||||
super(
|
||||
new LoginToggle(
|
||||
/*We can show the actual upload button!*/
|
||||
uploadFlow,
|
||||
/* User not logged in*/ t.pleaseLogin.Clone(),
|
||||
state
|
||||
),
|
||||
undefined /* Nothing as the user badge is disabled*/,
|
||||
state?.featureSwitchUserbadge
|
||||
)
|
||||
}
|
||||
}
|
||||
48
src/UI/Image/SlideShow.ts
Normal file
48
src/UI/Image/SlideShow.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { Store } from "../../Logic/UIEventSource"
|
||||
import BaseUIElement from "../BaseUIElement"
|
||||
import { Utils } from "../../Utils"
|
||||
import Combine from "../Base/Combine"
|
||||
|
||||
export class SlideShow extends BaseUIElement {
|
||||
private readonly embeddedElements: Store<BaseUIElement[]>
|
||||
|
||||
constructor(embeddedElements: Store<BaseUIElement[]>) {
|
||||
super()
|
||||
this.embeddedElements = embeddedElements
|
||||
this.SetStyle("scroll-snap-type: x mandatory; overflow-x: auto")
|
||||
}
|
||||
|
||||
protected InnerConstructElement(): HTMLElement {
|
||||
const el = document.createElement("div")
|
||||
el.style.minWidth = "min-content"
|
||||
el.style.display = "flex"
|
||||
el.style.justifyContent = "center"
|
||||
this.embeddedElements.addCallbackAndRun((elements) => {
|
||||
if (elements.length > 1) {
|
||||
el.style.justifyContent = "unset"
|
||||
}
|
||||
|
||||
while (el.firstChild) {
|
||||
el.removeChild(el.lastChild)
|
||||
}
|
||||
|
||||
elements = Utils.NoNull(elements).map((el) =>
|
||||
new Combine([el])
|
||||
.SetClass("block relative ml-1 bg-gray-200 m-1 rounded slideshow-item")
|
||||
.SetStyle(
|
||||
"min-width: 150px; width: max-content; height: var(--image-carousel-height);max-height: var(--image-carousel-height);scroll-snap-align: start;"
|
||||
)
|
||||
)
|
||||
|
||||
for (const element of elements ?? []) {
|
||||
el.appendChild(element.ConstructElement())
|
||||
}
|
||||
})
|
||||
|
||||
const wrapper = document.createElement("div")
|
||||
wrapper.style.maxWidth = "100%"
|
||||
wrapper.style.overflowX = "auto"
|
||||
wrapper.appendChild(el)
|
||||
return wrapper
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue