forked from MapComplete/MapComplete
Chore: rework image uploading, should work better now
This commit is contained in:
parent
6f5b0622a5
commit
94ba18785d
17 changed files with 548 additions and 238 deletions
|
|
@ -0,0 +1,40 @@
|
|||
<script lang="ts">
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let accept: string;
|
||||
export let multiple: boolean = true;
|
||||
|
||||
const dispatcher = createEventDispatcher<{ submit: FileList }>();
|
||||
export let cls: string = "";
|
||||
let drawAttention = false;
|
||||
let inputElement: HTMLInputElement;
|
||||
let id = Math.random() * 1000000000 + "";
|
||||
</script>
|
||||
|
||||
<form>
|
||||
<label class={twMerge(cls, drawAttention ? "glowing-shadow" : "")} for={"fileinput"+id}>
|
||||
<slot />
|
||||
|
||||
</label>
|
||||
<input {accept} bind:this={inputElement} class="hidden" id={"fileinput" + id} {multiple} name="file-input"
|
||||
on:change|preventDefault={() => {
|
||||
drawAttention = false;
|
||||
dispatcher("submit", inputElement.files)}}
|
||||
|
||||
on:dragend={ () => {drawAttention = false}}
|
||||
on:dragover|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Dragging over!")
|
||||
drawAttention = true
|
||||
e.dataTransfer.drop = "copy"
|
||||
}}
|
||||
on:dragstart={ () => {drawAttention = false}}
|
||||
on:drop|preventDefault|stopPropagation={(e) => {
|
||||
console.log("Got a 'drop'")
|
||||
drawAttention = false
|
||||
dispatcher("submit", e.dataTransfer.files)
|
||||
}}
|
||||
type="file"
|
||||
>
|
||||
</form>
|
||||
|
|
@ -1,9 +1,12 @@
|
|||
<script>
|
||||
import ToSvelte from "./ToSvelte.svelte"
|
||||
import Svg from "../../Svg"
|
||||
<script lang="ts">
|
||||
import ToSvelte from "./ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export let cls : string = undefined
|
||||
</script>
|
||||
|
||||
<div class="flex p-1 pl-2">
|
||||
<div class={twMerge( "flex p-1 pl-2", cls)}>
|
||||
<div class="min-w-6 h-6 w-6 animate-spin self-center">
|
||||
<ToSvelte construct={Svg.loading_svg()} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,8 +15,9 @@ import Loading from "../Base/Loading"
|
|||
import { LoginToggle } from "../Popup/LoginButton"
|
||||
import Constants from "../../Models/Constants"
|
||||
import { SpecialVisualizationState } from "../SpecialVisualization"
|
||||
import exp from "constants";
|
||||
|
||||
export class ImageUploadFlow extends Toggle {
|
||||
export class ImageUploadFlow extends Combine {
|
||||
private static readonly uploadCountsPerId = new Map<string, UIEventSource<number>>()
|
||||
|
||||
constructor(
|
||||
|
|
@ -129,7 +130,7 @@ export class ImageUploadFlow extends Toggle {
|
|||
uploader.uploadMany(title, description, filelist)
|
||||
})
|
||||
|
||||
const uploadFlow: BaseUIElement = new Combine([
|
||||
super([
|
||||
new VariableUiElement(
|
||||
uploader.queue
|
||||
.map((q) => q.length)
|
||||
|
|
@ -183,17 +184,9 @@ export class ImageUploadFlow extends Toggle {
|
|||
})
|
||||
.SetClass("underline"),
|
||||
]).SetStyle("font-size:small;"),
|
||||
]).SetClass("flex flex-col image-upload-flow mt-4 mb-8 text-center leading-none")
|
||||
])
|
||||
this.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">/**
|
||||
* Shows an 'upload'-button which will start the upload for this feature
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import UploadingImageCounter from "./UploadingImageCounter.svelte";
|
||||
import FileSelector from "../Base/FileSelector.svelte";
|
||||
import ToSvelte from "../Base/ToSvelte.svelte";
|
||||
import Svg from "../../Svg";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
export let image: string = undefined;
|
||||
if (image === "") {
|
||||
image = undefined;
|
||||
}
|
||||
export let labelText: string = undefined;
|
||||
const t = Translations.t.image;
|
||||
|
||||
let licenseStore = state.userRelatedState.imageLicense;
|
||||
|
||||
function handleFiles(files: FileList) {
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const file = files.item(i);
|
||||
console.log("Got file", file.name)
|
||||
try {
|
||||
state.imageUploadManager.uploadImageAndApply(file, tags.data);
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<LoginToggle {state}>
|
||||
|
||||
<Tr slot="not-logged-in" t={t.pleaseLogin} />
|
||||
<div class="flex flex-col">
|
||||
|
||||
<UploadingImageCounter {state} {tags} />
|
||||
<FileSelector accept="image/*" cls="button border-2 text-2xl" multiple={true}
|
||||
on:submit={e => handleFiles(e.detail)}>
|
||||
<div class="flex items-center">
|
||||
|
||||
{#if image !== undefined}
|
||||
<img src={image} />
|
||||
{:else}
|
||||
<ToSvelte construct={ Svg.camera_plus_svg().SetClass("block w-12 h-12 p-1 text-4xl ")} />
|
||||
{/if}
|
||||
{#if labelText}
|
||||
{labelText}
|
||||
{:else}
|
||||
<Tr t={t.addPicture} />
|
||||
{/if}
|
||||
</div>
|
||||
</FileSelector>
|
||||
<div class="text-sm">
|
||||
<Tr t={t.respectPrivacy} />
|
||||
<a class="cursor-pointer" on:click={() => {state.guistate.openUsersettings("picture-license")}}>
|
||||
<Tr t={t.currentLicense.Subs({license: $licenseStore})} />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</LoginToggle>
|
||||
|
|
@ -1,31 +1,67 @@
|
|||
<script lang="ts">/**
|
||||
* Shows an 'upload'-button which will start the upload for this feature
|
||||
* Shows information about how much images are uploaded for the given feature
|
||||
*/
|
||||
|
||||
import type { SpecialVisualizationState } from "../SpecialVisualization";
|
||||
import type { Feature } from "geojson";
|
||||
import { Store } from "../../Logic/UIEventSource";
|
||||
import type { OsmTags } from "../../Models/OsmFeature";
|
||||
import { ImageUploader } from "../../Logic/ImageProviders/ImageUploader";
|
||||
import LoginToggle from "../Base/LoginToggle.svelte";
|
||||
import Translations from "../i18n/Translations";
|
||||
import Tr from "../Base/Tr.svelte";
|
||||
import { ImageUploadManager } from "../../Logic/ImageProviders/ImageUploadManager";
|
||||
import Loading from "../Base/Loading.svelte";
|
||||
|
||||
export let state: SpecialVisualizationState;
|
||||
export let feature: Feature;
|
||||
|
||||
export let tags: Store<OsmTags>;
|
||||
export let state: SpecialVisualizationState;
|
||||
export let lon: number;
|
||||
export let lat: number;
|
||||
const t = Translations.t.image
|
||||
const featureId = tags.data.id;
|
||||
const {
|
||||
uploadStarted,
|
||||
uploadFinished,
|
||||
retried,
|
||||
failed
|
||||
} = state.imageUploadManager.getCountsFor(featureId);
|
||||
const t = Translations.t.image;
|
||||
|
||||
</script>
|
||||
|
||||
{#if $uploadStarted == 1}
|
||||
{#if $uploadFinished == 1 }
|
||||
<Tr cls="thanks" t={t.upload.one.done} />
|
||||
{:else if $failed == 1}
|
||||
<div class="flex flex-col alert">
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
{:else if $retried == 1}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.one.retrying} />
|
||||
</Loading>
|
||||
{:else }
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.one.uploading} />
|
||||
</Loading>
|
||||
{/if}
|
||||
{:else if $uploadStarted > 1}
|
||||
{#if ($uploadFinished + $failed) == $uploadStarted && $uploadFinished > 0}
|
||||
<Tr cls="thanks" t={t.upload.multiple.done.Subs({count: $uploadFinished})} />
|
||||
{:else if $uploadFinished == 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.uploading.Subs({count: $uploadStarted})} />
|
||||
</Loading>
|
||||
{:else if $uploadFinished > 0}
|
||||
<Loading cls="alert">
|
||||
<Tr t={t.upload.multiple.partiallyDone.Subs({count: $uploadStarted - $uploadFinished, done: $uploadFinished})} />
|
||||
</Loading>
|
||||
{/if}
|
||||
{#if $failed > 0}
|
||||
<div class="flex flex-col alert">
|
||||
{#if failed === 1}
|
||||
<Tr cls="self-center" t={t.upload.one.failed} />
|
||||
{:else}
|
||||
<Tr cls="self-center" t={t.upload.multiple.someFailed.Subs({count: $failed})} />
|
||||
|
||||
<LoginToggle>
|
||||
|
||||
<Tr slot="not-logged-in" t={t.pleaseLogin}/>
|
||||
|
||||
</LoginToggle>
|
||||
{/if}
|
||||
<Tr t={t.upload.failReasons} />
|
||||
<Tr t={t.upload.failReasonsAdvanced} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -1,113 +1,117 @@
|
|||
import { Store, UIEventSource } from "../Logic/UIEventSource"
|
||||
import BaseUIElement from "./BaseUIElement"
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"
|
||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource"
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection"
|
||||
import { Changes } from "../Logic/Osm/Changes"
|
||||
import { ExportableMap, MapProperties } from "../Models/MapProperties"
|
||||
import LayerState from "../Logic/State/LayerState"
|
||||
import { Feature, Geometry, Point } from "geojson"
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews"
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState"
|
||||
import { MenuState } from "../Models/MenuState"
|
||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader"
|
||||
import { RasterLayerPolygon } from "../Models/RasterLayers"
|
||||
import { Store, UIEventSource } from "../Logic/UIEventSource";
|
||||
import BaseUIElement from "./BaseUIElement";
|
||||
import LayoutConfig from "../Models/ThemeConfig/LayoutConfig";
|
||||
import { IndexedFeatureSource, WritableFeatureSource } from "../Logic/FeatureSource/FeatureSource";
|
||||
import { OsmConnection } from "../Logic/Osm/OsmConnection";
|
||||
import { Changes } from "../Logic/Osm/Changes";
|
||||
import { ExportableMap, MapProperties } from "../Models/MapProperties";
|
||||
import LayerState from "../Logic/State/LayerState";
|
||||
import { Feature, Geometry, Point } from "geojson";
|
||||
import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource";
|
||||
import { MangroveIdentity } from "../Logic/Web/MangroveReviews";
|
||||
import { GeoIndexedStoreForLayer } from "../Logic/FeatureSource/Actors/GeoIndexedStore";
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig";
|
||||
import FeatureSwitchState from "../Logic/State/FeatureSwitchState";
|
||||
import { MenuState } from "../Models/MenuState";
|
||||
import OsmObjectDownloader from "../Logic/Osm/OsmObjectDownloader";
|
||||
import { RasterLayerPolygon } from "../Models/RasterLayers";
|
||||
import { ImageUploadManager } from "../Logic/ImageProviders/ImageUploadManager";
|
||||
|
||||
/**
|
||||
* The state needed to render a special Visualisation.
|
||||
*/
|
||||
export interface SpecialVisualizationState {
|
||||
readonly guistate: MenuState
|
||||
readonly layout: LayoutConfig
|
||||
readonly featureSwitches: FeatureSwitchState
|
||||
readonly guistate: MenuState;
|
||||
readonly layout: LayoutConfig;
|
||||
readonly featureSwitches: FeatureSwitchState;
|
||||
|
||||
readonly layerState: LayerState
|
||||
readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> }
|
||||
readonly layerState: LayerState;
|
||||
readonly featureProperties: { getStore(id: string): UIEventSource<Record<string, string>> };
|
||||
|
||||
readonly indexedFeatures: IndexedFeatureSource
|
||||
readonly indexedFeatures: IndexedFeatureSource;
|
||||
|
||||
/**
|
||||
* Some features will create a new element that should be displayed.
|
||||
* These can be injected by appending them to this featuresource (and pinging it)
|
||||
*/
|
||||
readonly newFeatures: WritableFeatureSource
|
||||
/**
|
||||
* Some features will create a new element that should be displayed.
|
||||
* These can be injected by appending them to this featuresource (and pinging it)
|
||||
*/
|
||||
readonly newFeatures: WritableFeatureSource;
|
||||
|
||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>
|
||||
readonly historicalUserLocations: WritableFeatureSource<Feature<Point>>;
|
||||
|
||||
readonly osmConnection: OsmConnection
|
||||
readonly featureSwitchUserbadge: Store<boolean>
|
||||
readonly featureSwitchIsTesting: Store<boolean>
|
||||
readonly changes: Changes
|
||||
readonly osmObjectDownloader: OsmObjectDownloader
|
||||
/**
|
||||
* State of the main map
|
||||
*/
|
||||
readonly mapProperties: MapProperties & ExportableMap
|
||||
readonly osmConnection: OsmConnection;
|
||||
readonly featureSwitchUserbadge: Store<boolean>;
|
||||
readonly featureSwitchIsTesting: Store<boolean>;
|
||||
readonly changes: Changes;
|
||||
readonly osmObjectDownloader: OsmObjectDownloader;
|
||||
/**
|
||||
* State of the main map
|
||||
*/
|
||||
readonly mapProperties: MapProperties & ExportableMap;
|
||||
|
||||
readonly selectedElement: UIEventSource<Feature>
|
||||
/**
|
||||
* Works together with 'selectedElement' to indicate what properties should be displayed
|
||||
*/
|
||||
readonly selectedLayer: UIEventSource<LayerConfig>
|
||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>
|
||||
readonly selectedElement: UIEventSource<Feature>;
|
||||
/**
|
||||
* Works together with 'selectedElement' to indicate what properties should be displayed
|
||||
*/
|
||||
readonly selectedLayer: UIEventSource<LayerConfig>;
|
||||
readonly selectedElementAndLayer: Store<{ feature: Feature; layer: LayerConfig }>;
|
||||
|
||||
/**
|
||||
* If data is currently being fetched from external sources
|
||||
*/
|
||||
readonly dataIsLoading: Store<boolean>
|
||||
/**
|
||||
* Only needed for 'ReplaceGeometryAction'
|
||||
*/
|
||||
readonly fullNodeDatabase?: FullNodeDatabaseSource
|
||||
/**
|
||||
* If data is currently being fetched from external sources
|
||||
*/
|
||||
readonly dataIsLoading: Store<boolean>;
|
||||
/**
|
||||
* Only needed for 'ReplaceGeometryAction'
|
||||
*/
|
||||
readonly fullNodeDatabase?: FullNodeDatabaseSource;
|
||||
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>
|
||||
readonly userRelatedState: {
|
||||
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
|
||||
readonly mangroveIdentity: MangroveIdentity
|
||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||
readonly preferencesAsTags: Store<Record<string, string>>
|
||||
readonly language: UIEventSource<string>
|
||||
}
|
||||
readonly lastClickObject: WritableFeatureSource
|
||||
readonly perLayer: ReadonlyMap<string, GeoIndexedStoreForLayer>;
|
||||
readonly userRelatedState: {
|
||||
readonly imageLicense: UIEventSource<string>;
|
||||
readonly showTags: UIEventSource<"no" | undefined | "always" | "yes" | "full">
|
||||
readonly mangroveIdentity: MangroveIdentity
|
||||
readonly showAllQuestionsAtOnce: UIEventSource<boolean>
|
||||
readonly preferencesAsTags: Store<Record<string, string>>
|
||||
readonly language: UIEventSource<string>
|
||||
};
|
||||
readonly lastClickObject: WritableFeatureSource;
|
||||
|
||||
readonly availableLayers: Store<RasterLayerPolygon[]>
|
||||
readonly availableLayers: Store<RasterLayerPolygon[]>;
|
||||
|
||||
readonly imageUploadManager: ImageUploadManager;
|
||||
}
|
||||
|
||||
export interface SpecialVisualization {
|
||||
readonly funcName: string
|
||||
readonly docs: string | BaseUIElement
|
||||
readonly example?: string
|
||||
readonly funcName: string;
|
||||
readonly docs: string | BaseUIElement;
|
||||
readonly example?: string;
|
||||
|
||||
/**
|
||||
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
||||
*/
|
||||
readonly needsNodeDatabase?: boolean
|
||||
readonly args: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
doc: string
|
||||
required?: false | boolean
|
||||
}[]
|
||||
readonly getLayerDependencies?: (argument: string[]) => string[]
|
||||
/**
|
||||
* Indicates that this special visualisation will make requests to the 'alLNodesDatabase' and that it thus should be included
|
||||
*/
|
||||
readonly needsNodeDatabase?: boolean;
|
||||
readonly args: {
|
||||
name: string
|
||||
defaultValue?: string
|
||||
doc: string
|
||||
required?: false | boolean
|
||||
}[];
|
||||
readonly getLayerDependencies?: (argument: string[]) => string[];
|
||||
|
||||
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[]
|
||||
structuredExamples?(): { feature: Feature<Geometry, Record<string, string>>; args: string[] }[];
|
||||
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement
|
||||
constr(
|
||||
state: SpecialVisualizationState,
|
||||
tagSource: UIEventSource<Record<string, string>>,
|
||||
argument: string[],
|
||||
feature: Feature,
|
||||
layer: LayerConfig
|
||||
): BaseUIElement;
|
||||
}
|
||||
|
||||
export type RenderingSpecification =
|
||||
| string
|
||||
| {
|
||||
func: SpecialVisualization
|
||||
args: string[]
|
||||
style: string
|
||||
}
|
||||
| string
|
||||
| {
|
||||
func: SpecialVisualization
|
||||
args: string[]
|
||||
style: string
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ import LiveQueryHandler from "../Logic/Web/LiveQueryHandler"
|
|||
import { SubtleButton } from "./Base/SubtleButton"
|
||||
import Svg from "../Svg"
|
||||
import NoteCommentElement from "./Popup/NoteCommentElement"
|
||||
import ImgurUploader from "../Logic/ImageProviders/ImgurUploader"
|
||||
import FileSelectorButton from "./Input/FileSelectorButton"
|
||||
import { LoginToggle } from "./Popup/LoginButton"
|
||||
import Toggle from "./Input/Toggle"
|
||||
|
|
@ -74,6 +73,7 @@ import FediverseValidator from "./InputElement/Validators/FediverseValidator"
|
|||
import SendEmail from "./Popup/SendEmail.svelte"
|
||||
import NearbyImages from "./Popup/NearbyImages.svelte"
|
||||
import NearbyImagesCollapsed from "./Popup/NearbyImagesCollapsed.svelte"
|
||||
import UploadImage from "./Image/UploadImage.svelte";
|
||||
|
||||
class NearbyImageVis implements SpecialVisualization {
|
||||
// Class must be in SpecialVisualisations due to weird cyclical import that breaks the tests
|
||||
|
|
@ -616,16 +616,19 @@ export default class SpecialVisualizations {
|
|||
{
|
||||
name: "image-key",
|
||||
doc: "Image tag to add the URL to (or image-tag:0, image-tag:1 when multiple images are added)",
|
||||
defaultValue: "image",
|
||||
required: false
|
||||
},
|
||||
{
|
||||
name: "label",
|
||||
doc: "The text to show on the button",
|
||||
defaultValue: "Add image",
|
||||
required: false
|
||||
},
|
||||
],
|
||||
constr: (state, tags, args) => {
|
||||
return new ImageUploadFlow(tags, state, args[0], args[1])
|
||||
return new SvelteUIElement(UploadImage, {
|
||||
state,tags, labelText: args[1], image: args[0]
|
||||
})
|
||||
// return new ImageUploadFlow(tags, state, args[0], args[1])
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue