Skip to content
Commits on Source (3)
## [1.4.4](https://code.castopod.org/adaures/castopod/compare/v1.4.3...v1.4.4) (2023-07-02)
### Bug Fixes
- **audio-clipper:** init segment position on firstUpdate + improve UX by adding
ghost handle
([aa68386](https://code.castopod.org/adaures/castopod/commit/aa683866671d14c0b9a11b09c74eb132673e5547)),
closes [#351](https://code.castopod.org/adaures/castopod/issues/351)
- set resized images to 72dpi for compatibility with Apple Podcasts
([0b327cb](https://code.castopod.org/adaures/castopod/commit/0b327cb4d9c92d0ae227a0f08ede3b29390df172)),
closes [#282](https://code.castopod.org/adaures/castopod/issues/282)
## [1.4.3](https://code.castopod.org/adaures/castopod/compare/v1.4.2...v1.4.3) (2023-06-29)
### Bug Fixes
......
......@@ -11,7 +11,7 @@ declare(strict_types=1);
|
| NOTE: this constant is updated upon release with Continuous Integration.
*/
defined('CP_VERSION') || define('CP_VERSION', '1.4.3');
defined('CP_VERSION') || define('CP_VERSION', '1.4.4');
/*
| --------------------------------------------------------------------
......
......@@ -10,7 +10,6 @@ const Clipboard = (): void => {
);
if (element) {
button.addEventListener("click", () => {
console.log(element);
element.select();
element.setSelectionRange(0, element.value.length);
document.execCommand("copy");
......
......@@ -9,6 +9,7 @@ const MultiSelect = (): void => {
const multiSelect = multiSelects[i];
new Choices(multiSelect, {
allowHTML: false,
maxItemCount: parseInt(multiSelect.dataset.maxItemCount || "-1"),
loadingText: multiSelect.dataset.loadingText,
itemSelectText: multiSelect.dataset.selectText,
......
......@@ -10,6 +10,7 @@ const Select = (): void => {
const select = selects[i];
new Choices(select, {
allowHTML: false,
loadingText: select.dataset.loadingText,
itemSelectText: select.dataset.selectText,
maxItemText: select.dataset.maxItemText,
......
......@@ -4,7 +4,7 @@ import {
property,
query,
queryAll,
queryAssignedNodes,
queryAssignedElements,
state,
} from "lit/decorators.js";
import WaveSurfer from "wavesurfer.js";
......@@ -27,14 +27,14 @@ interface EventElement {
@customElement("audio-clipper")
export class AudioClipper extends LitElement {
@queryAssignedNodes("audio", true)
_audio!: NodeListOf<HTMLAudioElement>;
@queryAssignedElements({ slot: "audio", flatten: true })
_audio!: Array<HTMLAudioElement>;
@queryAssignedNodes("start_time", true)
_startTimeInput!: NodeListOf<HTMLInputElement>;
@queryAssignedElements({ slot: "start_time", flatten: true })
_startTimeInput!: Array<HTMLInputElement>;
@queryAssignedNodes("duration", true)
_durationInput!: NodeListOf<HTMLInputElement>;
@queryAssignedElements({ slot: "duration", flatten: true })
_durationInput!: Array<HTMLInputElement>;
@query(".slider")
_sliderNode!: HTMLDivElement;
......@@ -45,9 +45,12 @@ export class AudioClipper extends LitElement {
@query(".slider__segment-content")
_segmentContentNode!: HTMLDivElement;
@query(".slider__segment-progress-handle")
@query(".slider__segment-progress-handle--main")
_progressNode!: HTMLDivElement;
@query(".slider__segment-progress-handle--ghost")
_progressGhostNode!: HTMLDivElement;
@query(".slider__seeking-placeholder")
_seekingNode!: HTMLDivElement;
......@@ -60,6 +63,9 @@ export class AudioClipper extends LitElement {
@queryAll(".slider__segment-handle")
_segmentHandleNodes!: NodeListOf<HTMLButtonElement>;
@property({ type: Number, attribute: "audio-duration" })
audioDuration = 0;
@property({ type: Number, attribute: "start-time" })
initStartTime = 0;
......@@ -81,6 +87,9 @@ export class AudioClipper extends LitElement {
@property({ attribute: "trim-end-label" })
trimEndLabel = "Trim end";
@state()
_canInteract = false;
@state()
_isPlaying = false;
......@@ -93,9 +102,6 @@ export class AudioClipper extends LitElement {
@state()
_action: Action | null = null;
@state()
_audioDuration = 0;
@state()
_sliderWidth = 0;
......@@ -116,7 +122,15 @@ export class AudioClipper extends LitElement {
_windowEvents: EventElement[] = [
{
events: ["load", "resize"],
events: ["load"],
onEvent: () => {
this._canInteract = true;
this._sliderWidth = this._sliderNode.clientWidth;
this.setSegmentPosition();
},
},
{
events: ["resize"],
onEvent: () => {
this._sliderWidth = this._sliderNode.clientWidth;
this.setSegmentPosition();
......@@ -130,9 +144,12 @@ export class AudioClipper extends LitElement {
onEvent: () => {
if (this._action !== null) {
document.body.style.cursor = "";
if (this._action.type === ActionType.Seek && this._seekingTime) {
if (
this._action.type === ActionType.Seek &&
this._seekingTime !== null
) {
this._audio[0].currentTime = this._seekingTime;
this._seekingTime = 0;
this._seekingTime = null;
}
this._action = null;
}
......@@ -141,14 +158,24 @@ export class AudioClipper extends LitElement {
{
events: ["mousemove"],
onEvent: (event: Event) => {
if (this._action !== null) {
this.updatePosition(event as MouseEvent);
}
this.updatePosition(event as MouseEvent);
},
},
];
_audioEvents: EventElement[] = [
{
events: ["loadedmetadata"],
onEvent: () => {
this.audioDuration = this._audio[0].duration;
},
},
{
events: ["waiting"],
onEvent: () => {
this._isBuffering = true;
},
},
{
events: ["play"],
onEvent: () => {
......@@ -176,7 +203,7 @@ export class AudioClipper extends LitElement {
);
context.fillStyle = "#04AC64";
const inc = this._bufferingBarNode.width / this._audio[0].duration;
const inc = this._bufferingBarNode.width / this.audioDuration;
for (let i = 0; i < this._audio[0].buffered.length; i++) {
const startX = this._audio[0].buffered.start(i) * inc;
......@@ -192,13 +219,11 @@ export class AudioClipper extends LitElement {
{
events: ["timeupdate"],
onEvent: () => {
// TODO: change this?
this._currentTime = parseFloat(this._audio[0].currentTime.toFixed(3));
this._currentTime = this._audio[0].currentTime;
if (this._currentTime > this._clip.endTime) {
this.pause();
this._audio[0].currentTime = this._clip.endTime;
} else if (this._currentTime < this._clip.startTime) {
this._isBuffering = true;
this._audio[0].currentTime = this._clip.startTime;
} else {
this._isBuffering = false;
......@@ -233,6 +258,21 @@ export class AudioClipper extends LitElement {
},
];
_sliderSegmentEvents: EventElement[] = [
{
events: ["hover"],
onEvent: (event: Event) => {
const ghostHandle = (event.target as HTMLDivElement).querySelector(
".segment"
) as HTMLDivElement;
if (ghostHandle) {
ghostHandle.style.opacity = "1";
ghostHandle.style.transform = "translateX(50)";
}
},
},
];
connectedCallback(): void {
super.connectedCallback();
......@@ -244,7 +284,9 @@ export class AudioClipper extends LitElement {
}
protected firstUpdated(): void {
this._audioDuration = this._audio[0].duration;
this._sliderWidth = this._sliderNode.clientWidth;
this.setSegmentPosition();
this._audio[0].volume = this._volume;
this._startTimeInput[0].hidden = true;
this._durationInput[0].hidden = true;
......@@ -255,7 +297,6 @@ export class AudioClipper extends LitElement {
interact: false,
barWidth: 2,
barHeight: 1,
// barGap: 4,
responsive: true,
waveColor: "hsl(0 5% 85%)",
cursorColor: "transparent",
......@@ -338,11 +379,11 @@ export class AudioClipper extends LitElement {
}
private getPositionFromSeconds(seconds: number) {
return (seconds * this._sliderWidth) / this._audioDuration;
return (seconds * this._sliderWidth) / this.audioDuration;
}
private getSecondsFromPosition(position: number) {
return (this._audioDuration * position) / this._sliderWidth;
return (this.audioDuration * position) / this._sliderWidth;
}
protected updated(
......@@ -405,14 +446,14 @@ export class AudioClipper extends LitElement {
}
case ActionType.StretchRight: {
let endTime;
if (seconds < this._audioDuration) {
if (seconds < this.audioDuration) {
if (seconds < this._clip.startTime + this.minDuration) {
endTime = this._clip.startTime + this.minDuration;
} else {
endTime = seconds;
}
} else {
endTime = this._audioDuration;
endTime = this.audioDuration;
}
this._clip = {
......@@ -459,6 +500,7 @@ export class AudioClipper extends LitElement {
const seekingTimePercentage =
(seekingTimeSegmentPosition / this._segmentContentNode.clientWidth) *
this._segmentContentNode.clientWidth;
this._progressNode.style.transform = `translateX(${seekingTimeSegmentPosition}px)`;
this._seekingNode.style.transform = `scaleX(${seekingTimePercentage})`;
}
......@@ -586,6 +628,10 @@ export class AudioClipper extends LitElement {
border-top: 10px solid #3b82f6;
}
.slider__segment-progress-handle--ghost {
opacity: 0.5;
}
.slider__segment .slider__segment-handle {
position: absolute;
width: 1rem;
......@@ -742,14 +788,19 @@ export class AudioClipper extends LitElement {
<slot name="audio"></slot>
<slot name="start_time"></slot>
<slot name="duration"></slot>
<div class="slider-wrapper" style="height:${this.height}">
<div class="slider-wrapper" style="height:${this.height}px">
<div id="waveform"></div>
<div class="slider" role="slider">
<div class="slider__segment--wrapper">
<div
class="slider__segment-progress-handle"
@mousedown="${(event: MouseEvent) =>
this.setAction(event, { type: ActionType.Seek })}"
class="slider__segment-progress-handle slider__segment-progress-handle--main"
@mousedown="${(event: MouseEvent) => {
this.setAction(event, { type: ActionType.Seek });
}}"
></div>
<div
class="slider__segment-progress-handle slider__segment-progress-handle--ghost"
?hidden=${true}
></div>
<div class="slider__segment">
<button
......@@ -764,6 +815,16 @@ export class AudioClipper extends LitElement {
<div class="slider__seeking-placeholder"></div>
<div
class="slider__segment-content"
@mousemove="${(event: MouseEvent) => {
const seekingTimeSegmentPosition =
event.clientX -
(event.target as HTMLDivElement).getBoundingClientRect()
.left;
this._progressGhostNode.hidden = false;
this._progressGhostNode.style.transform = `translateX(${seekingTimeSegmentPosition}px)`;
}}"
@mouseleave="${() => (this._progressGhostNode.hidden = true)}"
@mousedown="${(event: MouseEvent) =>
this.setAction(event, { type: ActionType.Seek })}"
@click="${(event: MouseEvent) => this.goTo(event)}"
......@@ -786,7 +847,7 @@ export class AudioClipper extends LitElement {
class="toolbar__play-button"
@click="${this._isPlaying ? this.pause : this.play}"
>
${this._isBuffering
${this._isBuffering || !this._canInteract
? html`<svg
class="animate-spin"
xmlns="http://www.w3.org/2000/svg"
......
......@@ -9,10 +9,10 @@ export class MarkdownPreview extends LitElement {
@property()
for!: string;
@state()
@property({ attribute: false })
_textarea!: HTMLTextAreaElement;
@state()
@property({ attribute: false })
_markdownToolbar!: MarkdownToolbarElement;
@state()
......@@ -49,6 +49,8 @@ export class MarkdownPreview extends LitElement {
return marked(this.escapeHtml(this._textarea.value), {
renderer: renderer,
headerIds: false,
mangle: false,
});
}
......
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
import {
customElement,
property,
queryAssignedElements,
} from "lit/decorators.js";
import { MarkdownPreview } from "./markdown-preview";
@customElement("markdown-write-preview")
......@@ -7,17 +11,17 @@ export class MarkdownWritePreview extends LitElement {
@property()
for!: string;
@property()
@property({ attribute: false })
_textarea: HTMLTextAreaElement | null = null;
@property()
@property({ attribute: false })
_markdownPreview!: MarkdownPreview;
@queryAssignedNodes("write", true)
_write!: NodeListOf<HTMLButtonElement>;
@queryAssignedElements({ slot: "write", flatten: true })
_write!: Array<HTMLButtonElement>;
@queryAssignedNodes("preview", true)
_preview!: NodeListOf<HTMLButtonElement>;
@queryAssignedElements({ slot: "preview", flatten: true })
_preview!: Array<HTMLButtonElement>;
connectedCallback(): void {
super.connectedCallback();
......
import "@github/clipboard-copy-element";
import ClipboardCopyElement from "@github/clipboard-copy-element";
import { css, html, LitElement, TemplateResult } from "lit";
import {
customElement,
property,
query,
queryAssignedNodes,
queryAssignedElements,
state,
} from "lit/decorators.js";
@customElement("permalink-edit")
export class PermalinkEdit extends LitElement {
@queryAssignedNodes("domain", true)
@queryAssignedElements({ slot: "domain", flatten: true })
_domain!: NodeListOf<HTMLSpanElement>;
@queryAssignedNodes("slug-input", true)
@queryAssignedElements({ slot: "slug-input", flatten: true })
_slugInput!: NodeListOf<HTMLInputElement>;
@query("clipboard-copy")
_clipboardCopy!: any;
_clipboardCopy!: ClipboardCopyElement;
@property({ attribute: "edit-label" })
editLabel = "Edit";
......
......@@ -27,15 +27,15 @@ export class PlayEpisodeButton extends LitElement {
@property()
playingLabel!: string;
@property()
isPlaying!: boolean;
@property()
@property({ attribute: false })
_castopodAudioPlayer!: HTMLDivElement;
@property()
@property({ attribute: false })
_audio!: HTMLAudioElement;
@state()
isPlaying!: boolean;
@state()
_playbackSpeed = 1;
......
......@@ -44,11 +44,6 @@ export class PlaySoundbite extends LitElement {
name: "timeupdate",
onEvent: () => {
if (this._audio) {
console.log(
this._audio.currentTime,
this.startTime,
this.startTime + this.duration
);
if (this._audio.currentTime < this.startTime) {
this._isLoading = true;
this._audio.currentTime = this.startTime;
......
......@@ -2,8 +2,7 @@ import { css, html, LitElement, TemplateResult } from "lit";
import {
customElement,
property,
queryAssignedNodes,
state,
queryAssignedElements,
} from "lit/decorators.js";
import { styleMap } from "lit/directives/style-map.js";
......@@ -21,7 +20,7 @@ const formatMap = {
@customElement("video-clip-previewer")
export class VideoClipPreviewer extends LitElement {
@queryAssignedNodes("preview_image", true)
@queryAssignedElements({ slot: "preview_image", flatten: true })
_image!: NodeListOf<HTMLImageElement>;
@property()
......@@ -36,7 +35,7 @@ export class VideoClipPreviewer extends LitElement {
@property({ type: Number })
duration!: number;
@state()
@property({ attribute: false })
_previewImage!: HTMLImageElement;
protected firstUpdated(): void {
......
{
"name": "adaures/castopod",
"version": "1.4.3",
"version": "1.4.4",
"type": "project",
"description": "Castopod is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
"homepage": "https://castopod.org",
......
......@@ -136,10 +136,17 @@ class Image extends BaseMedia
foreach ($this->sizes as $name => $size) {
$tempFilePath = tempnam(WRITEPATH . 'temp', 'img_');
$imageService
$resizedImage = $imageService
->withFile($this->attributes['file']->getRealPath())
->resize($size['width'], $size['height'])
->save($tempFilePath);
->resize($size['width'], $size['height']);
$resizedImageResource = $resizedImage->getResource();
// set resolution to 72 by 72 for all sizes
// Apple Podcasts requires images to be 72 dpi
imageresolution($resizedImageResource, 72, 72);
$resizedImage->save($tempFilePath);
$newImage = new File($tempFilePath, true);
......
{
"name": "castopod",
"version": "1.4.3",
"version": "1.4.4",
"description": "Castopod Host is an open-source hosting platform made for podcasters who want engage and interact with their audience.",
"private": true,
"license": "AGPL-3.0-or-later",
......@@ -34,7 +34,7 @@
"@codemirror/language": "^6.8.0",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.14.0",
"@floating-ui/dom": "^1.4.2",
"@floating-ui/dom": "^1.4.3",
"@github/clipboard-copy-element": "^1.2.1",
"@github/hotkey": "^2.0.1",
"@github/markdown-toolbar-element": "^2.1.1",
......@@ -52,8 +52,8 @@
"xml-formatter": "^3.4.1"
},
"devDependencies": {
"@commitlint/cli": "^17.6.5",
"@commitlint/config-conventional": "^17.6.5",
"@commitlint/cli": "^17.6.6",
"@commitlint/config-conventional": "^17.6.6",
"@csstools/css-tokenizer": "^2.1.1",
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/exec": "^6.0.3",
......@@ -64,19 +64,19 @@
"@types/leaflet": "^1.9.3",
"@types/marked": "^5.0.0",
"@types/wavesurfer.js": "^6.0.6",
"@typescript-eslint/eslint-plugin": "^5.60.0",
"@typescript-eslint/parser": "^5.60.0",
"@typescript-eslint/eslint-plugin": "^5.60.1",
"@typescript-eslint/parser": "^5.60.1",
"all-contributors-cli": "^6.26.0",
"commitizen": "^4.3.0",
"cross-env": "^7.0.3",
"cssnano": "^6.0.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^8.43.0",
"eslint": "^8.44.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"husky": "^8.0.3",
"is-ci": "^3.0.1",
"lint-staged": "^13.2.2",
"lint-staged": "^13.2.3",
"postcss": "^8.4.24",
"postcss-import": "^15.1.0",
"postcss-nesting": "^11.3.0",
......@@ -84,12 +84,12 @@
"postcss-reporter": "^7.0.5",
"prettier": "2.8.8",
"prettier-plugin-organize-imports": "^3.2.2",
"semantic-release": "^21.0.5",
"semantic-release": "^21.0.6",
"stylelint": "^15.9.0",
"stylelint-config-standard": "^33.0.0",
"svgo": "^3.0.2",
"tailwindcss": "^3.3.2",
"typescript": "^5.1.3",
"typescript": "^5.1.6",
"vite": "^4.3.9",
"vite-plugin-pwa": "^0.16.4",
"workbox-build": "^7.0.0",
......
This diff is collapsed.
......@@ -20,8 +20,8 @@
required="true"
class="max-w-sm"
/>
<audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>" class="mt-8">
<audio slot="audio" src="<?= $episode->audio_url ?>" preload="auto">
<audio-clipper start-time="<?= old('start_time', 0) ?>" audio-duration="<?= $episode->audio->duration ?>" duration="<?= old('duration', $episode->audio->duration >= 60 ? 60 : $episode->audio->duration) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>" class="mt-8">
<audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
Your browser does not support the <code>audio</code> element.
</audio>
<input slot="start_time" type="number" name="start_time" placeholder="<?= lang('VideoClip.form.start_time') ?>" step="0.001" />
......
......@@ -17,7 +17,7 @@
<video-clip-previewer duration="<?= old('duration', 30) ?>">
<img slot="preview_image" src="<?= $episode->cover->thumbnail_url ?>" alt="<?= $episode->cover->description ?>" loading="lazy" />
</video-clip-previewer>
<audio-clipper start-time="<?= old('start_time', 0) ?>" duration="<?= old('duration', 30) ?>" min-duration="10" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>">
<audio-clipper start-time="<?= old('start_time', 0) ?>" audio-duration="<?= $episode->audio->duration ?>" duration="<?= old('duration', $episode->audio->duration >= 60 ? 60 : $episode->audio->duration) ?>" volume=".5" height="50" trim-start-label="<?= lang('VideoClip.form.trim_start') ?>" trim-end-label="<?= lang('VideoClip.form.trim_end') ?>">
<audio slot="audio" src="<?= $episode->audio->file_url ?>" preload="auto">
Your browser does not support the <code>audio</code> element.
</audio>
......