feat: add sequential progression and video watch lesson completion
Gate student content by completion order, track per-video watch progress with seek enforcement, and auto-complete lessons at the configured threshold. Show ring progress in the lesson header, keep watch progress on rewatch, and sync progression locks client-side so the next item unlocks after completion.
This commit is contained in:
@@ -75,6 +75,16 @@ class ImageCropperRootState {
|
||||
this.pixelCrop = undefined;
|
||||
}
|
||||
|
||||
onUseOriginal() {
|
||||
if (!this.tempUrl) return;
|
||||
|
||||
this.opts.src.current = this.tempUrl;
|
||||
this.open = false;
|
||||
this.opts.onCropped.current(this.tempUrl);
|
||||
this.tempUrl = undefined;
|
||||
this.pixelCrop = undefined;
|
||||
}
|
||||
|
||||
async onCrop() {
|
||||
if (!this.pixelCrop || !this.tempUrl) return;
|
||||
|
||||
@@ -146,6 +156,16 @@ class ImageCropperCancelState {
|
||||
}
|
||||
}
|
||||
|
||||
class ImageCropperUseOriginalState {
|
||||
constructor(readonly rootState: ImageCropperRootState) {
|
||||
this.onclick = this.onclick.bind(this);
|
||||
}
|
||||
|
||||
onclick() {
|
||||
this.rootState.onUseOriginal();
|
||||
}
|
||||
}
|
||||
|
||||
const ImageCropperRootContext = new Context<ImageCropperRootState>('ImageCropper.Root');
|
||||
|
||||
export const useImageCropperRoot = (props: ImageCropperRootProps) => {
|
||||
@@ -187,3 +207,9 @@ export const useImageCropperCancel = () => {
|
||||
|
||||
return new ImageCropperCancelState(rootState);
|
||||
};
|
||||
|
||||
export const useImageCropperUseOriginal = () => {
|
||||
const rootState = ImageCropperRootContext.get();
|
||||
|
||||
return new ImageCropperUseOriginalState(rootState);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '../../base/dialog';
|
||||
import Button from '../../base/button/button.svelte';
|
||||
import { cn } from '../../tools';
|
||||
import { useImageCropperDialog } from './image-cropper-context.svelte';
|
||||
import type { ImageCropperDialogProps } from './types';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
|
||||
let { children, class: className, ...rest }: ImageCropperDialogProps = $props();
|
||||
|
||||
@@ -12,12 +14,22 @@
|
||||
<Dialog.Root bind:open={dialogState.rootState.open}>
|
||||
<Dialog.Content
|
||||
{...rest}
|
||||
showCloseButton
|
||||
showCloseButton={false}
|
||||
class={cn(
|
||||
'ui:min-h-96 ui:max-w-[60%] ui:rounded-none ui:border-x-0 ui:sm:ui:max-w-lg ui:sm:ui:rounded-lg ui:sm:ui:border-x',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
class="ui:absolute ui:end-4 ui:top-4"
|
||||
onclick={() => dialogState.rootState.onCancel()}
|
||||
>
|
||||
<XIcon />
|
||||
<span class="ui:sr-only">Close</span>
|
||||
</Button>
|
||||
<div class="ui:flex ui:flex-col ui:gap-4">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import Button, { type ButtonElementProps } from '../../base/button/button.svelte';
|
||||
import { useImageCropperUseOriginal } from './image-cropper-context.svelte';
|
||||
import ImageIcon from '@lucide/svelte/icons/image';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
variant = 'outline',
|
||||
size = 'sm',
|
||||
onclick,
|
||||
children,
|
||||
...rest
|
||||
}: ButtonElementProps = $props();
|
||||
|
||||
const useOriginalState = useImageCropperUseOriginal();
|
||||
</script>
|
||||
|
||||
<Button
|
||||
{...rest}
|
||||
bind:ref
|
||||
{size}
|
||||
{variant}
|
||||
onclick={(
|
||||
e: MouseEvent & {
|
||||
currentTarget: EventTarget & HTMLButtonElement;
|
||||
}
|
||||
) => {
|
||||
onclick?.(e);
|
||||
|
||||
useOriginalState.onclick();
|
||||
}}
|
||||
>
|
||||
<ImageIcon />
|
||||
{#if children}
|
||||
{@render children()}
|
||||
{:else}
|
||||
<span>Don't Crop</span>
|
||||
{/if}
|
||||
</Button>
|
||||
@@ -6,8 +6,9 @@ import Cropper from './image-cropper-cropper.svelte';
|
||||
import Controls from './image-cropper-controls.svelte';
|
||||
import Crop from './image-cropper-crop.svelte';
|
||||
import Cancel from './image-cropper-cancel.svelte';
|
||||
import UseOriginal from './image-cropper-use-original.svelte';
|
||||
import { getFileFromUrl } from './utils';
|
||||
|
||||
export { Root, UploadTrigger, Preview, Dialog, Cropper, Controls, Crop, Cancel, getFileFromUrl };
|
||||
export { Root, UploadTrigger, Preview, Dialog, Cropper, Controls, Crop, Cancel, UseOriginal, getFileFromUrl };
|
||||
|
||||
export type * from './types';
|
||||
|
||||
Reference in New Issue
Block a user