feat: add media save guide
This commit is contained in:
@@ -2,6 +2,7 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router-dom";
|
|||||||
import { I18nProvider } from "./i18n";
|
import { I18nProvider } from "./i18n";
|
||||||
import { MotionProvider } from "./motion";
|
import { MotionProvider } from "./motion";
|
||||||
import { ToastProvider } from "./components/Toast";
|
import { ToastProvider } from "./components/Toast";
|
||||||
|
import { SaveToAlbumGuideProvider } from "./components/SaveToAlbumGuide";
|
||||||
import { PublicLayout } from "./layouts/PublicLayout";
|
import { PublicLayout } from "./layouts/PublicLayout";
|
||||||
import { LocalizedHomePage } from "./pages/LocalizedHome";
|
import { LocalizedHomePage } from "./pages/LocalizedHome";
|
||||||
import { Browse } from "./pages/Browse";
|
import { Browse } from "./pages/Browse";
|
||||||
@@ -27,6 +28,7 @@ export default function App() {
|
|||||||
<I18nProvider>
|
<I18nProvider>
|
||||||
<MotionProvider>
|
<MotionProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
|
<SaveToAlbumGuideProvider>
|
||||||
<AdminRouterModeProvider value="absolute">
|
<AdminRouterModeProvider value="absolute">
|
||||||
<ImageLightboxProvider>
|
<ImageLightboxProvider>
|
||||||
<VideoPlayerProvider>
|
<VideoPlayerProvider>
|
||||||
@@ -108,6 +110,7 @@ export default function App() {
|
|||||||
</VideoPlayerProvider>
|
</VideoPlayerProvider>
|
||||||
</ImageLightboxProvider>
|
</ImageLightboxProvider>
|
||||||
</AdminRouterModeProvider>
|
</AdminRouterModeProvider>
|
||||||
|
</SaveToAlbumGuideProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</MotionProvider>
|
</MotionProvider>
|
||||||
</I18nProvider>
|
</I18nProvider>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { formatDateYmd } from "../utils/format";
|
|||||||
import { postToResource } from "../utils/postResourceAdapter";
|
import { postToResource } from "../utils/postResourceAdapter";
|
||||||
import type { Post } from "../types/post";
|
import type { Post } from "../types/post";
|
||||||
import { downloadAttachment } from "./messageStream/utils/downloadFile";
|
import { downloadAttachment } from "./messageStream/utils/downloadFile";
|
||||||
|
import { mediaSaveKindFromType, useSaveToAlbumGuide } from "./SaveToAlbumGuide";
|
||||||
import { useToast } from "./Toast";
|
import { useToast } from "./Toast";
|
||||||
|
|
||||||
const MEDALS = ["🥇", "🥈", "🥉"];
|
const MEDALS = ["🥇", "🥈", "🥉"];
|
||||||
@@ -94,6 +95,7 @@ function PopularRankRow({
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const lp = useLocalizedPath();
|
const lp = useLocalizedPath();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const [coverFailed, setCoverFailed] = useState(false);
|
const [coverFailed, setCoverFailed] = useState(false);
|
||||||
|
|
||||||
@@ -110,6 +112,8 @@ function PopularRankRow({
|
|||||||
r.downloadAttachmentId,
|
r.downloadAttachmentId,
|
||||||
r.title,
|
r.title,
|
||||||
);
|
);
|
||||||
|
const mediaKind = mediaSaveKindFromType(r.type);
|
||||||
|
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
||||||
} catch {
|
} catch {
|
||||||
showToast(t("downloadFail"), "error");
|
showToast(t("downloadFail"), "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
downloadAttachment,
|
downloadAttachment,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
} from "./messageStream/utils/downloadFile";
|
} from "./messageStream/utils/downloadFile";
|
||||||
|
import { mediaSaveKindFromType, useSaveToAlbumGuide } from "./SaveToAlbumGuide";
|
||||||
import { useToast } from "./Toast";
|
import { useToast } from "./Toast";
|
||||||
|
|
||||||
function isPlaceholderAsset(path: string | undefined | null) {
|
function isPlaceholderAsset(path: string | undefined | null) {
|
||||||
@@ -52,6 +53,7 @@ export function RecommendedCard({
|
|||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const lp = useLocalizedPath();
|
const lp = useLocalizedPath();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
const figmaCover =
|
const figmaCover =
|
||||||
officialRecommendationCoverFallbacks[
|
officialRecommendationCoverFallbacks[
|
||||||
@@ -87,6 +89,8 @@ export function RecommendedCard({
|
|||||||
} else {
|
} else {
|
||||||
await downloadFile(dl, displayTitle);
|
await downloadFile(dl, displayTitle);
|
||||||
}
|
}
|
||||||
|
const mediaKind = mediaSaveKindFromType(r.type);
|
||||||
|
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
||||||
} catch {
|
} catch {
|
||||||
showToast(t("downloadFail"), "error");
|
showToast(t("downloadFail"), "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
185
src/components/SaveToAlbumGuide.tsx
Normal file
185
src/components/SaveToAlbumGuide.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { X } from "lucide-react";
|
||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { createPortal } from "react-dom";
|
||||||
|
import { useI18n } from "../i18n";
|
||||||
|
import type { Attachment } from "../types/post";
|
||||||
|
|
||||||
|
export type SaveToAlbumMediaKind = "image" | "video";
|
||||||
|
|
||||||
|
type SaveToAlbumGuideContextValue = {
|
||||||
|
showSaveToAlbumGuide: (kind: SaveToAlbumMediaKind) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SaveToAlbumGuideContext =
|
||||||
|
createContext<SaveToAlbumGuideContextValue | null>(null);
|
||||||
|
|
||||||
|
export function useSaveToAlbumGuide(): SaveToAlbumGuideContextValue {
|
||||||
|
const ctx = useContext(SaveToAlbumGuideContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error(
|
||||||
|
"useSaveToAlbumGuide must be used within SaveToAlbumGuideProvider",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mediaSaveKindFromAttachment(
|
||||||
|
attachment: Attachment,
|
||||||
|
): SaveToAlbumMediaKind | null {
|
||||||
|
if (attachment.kind === "image" || attachment.mime.startsWith("image/")) {
|
||||||
|
return "image";
|
||||||
|
}
|
||||||
|
if (attachment.kind === "video" || attachment.mime.startsWith("video/")) {
|
||||||
|
return "video";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mediaSaveKindFromType(
|
||||||
|
type: string | undefined,
|
||||||
|
): SaveToAlbumMediaKind | null {
|
||||||
|
if (type === "image") return "image";
|
||||||
|
if (type === "video") return "video";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SavePlatform = "ios" | "android" | "desktop";
|
||||||
|
|
||||||
|
function detectSavePlatform(): SavePlatform {
|
||||||
|
if (typeof navigator === "undefined") return "desktop";
|
||||||
|
const ua = navigator.userAgent || "";
|
||||||
|
if (/android/i.test(ua)) return "android";
|
||||||
|
if (/iPad|iPhone|iPod/i.test(ua)) return "ios";
|
||||||
|
if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) {
|
||||||
|
return "ios";
|
||||||
|
}
|
||||||
|
return "desktop";
|
||||||
|
}
|
||||||
|
|
||||||
|
function platformStepKeys(platform: SavePlatform): string[] {
|
||||||
|
if (platform === "ios") {
|
||||||
|
return [
|
||||||
|
"saveAlbumGuideIosStep1",
|
||||||
|
"saveAlbumGuideIosStep2",
|
||||||
|
"saveAlbumGuideIosStep3",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if (platform === "android") {
|
||||||
|
return [
|
||||||
|
"saveAlbumGuideAndroidStep1",
|
||||||
|
"saveAlbumGuideAndroidStep2",
|
||||||
|
"saveAlbumGuideAndroidStep3",
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return ["saveAlbumGuideDesktopStep1", "saveAlbumGuideDesktopStep2"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveToAlbumGuideProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const [mediaKind, setMediaKind] = useState<SaveToAlbumMediaKind | null>(null);
|
||||||
|
const platform = useMemo(() => detectSavePlatform(), []);
|
||||||
|
|
||||||
|
const showSaveToAlbumGuide = useCallback((kind: SaveToAlbumMediaKind) => {
|
||||||
|
setMediaKind(kind);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const close = useCallback(() => setMediaKind(null), []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!mediaKind) return;
|
||||||
|
const onKey = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === "Escape") close();
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", onKey);
|
||||||
|
return () => window.removeEventListener("keydown", onKey);
|
||||||
|
}, [close, mediaKind]);
|
||||||
|
|
||||||
|
const value = useMemo(
|
||||||
|
() => ({ showSaveToAlbumGuide }),
|
||||||
|
[showSaveToAlbumGuide],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SaveToAlbumGuideContext.Provider value={value}>
|
||||||
|
{children}
|
||||||
|
{mediaKind
|
||||||
|
? createPortal(
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-[130] flex items-center justify-center bg-black/70 px-4 backdrop-blur-sm"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="save-album-guide-title"
|
||||||
|
onClick={close}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-md overflow-hidden rounded-3xl border border-white/10 bg-[#1c1c21] text-neutral-100 shadow-2xl shadow-black/70"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between gap-4 border-b border-white/10 px-5 py-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-ark-gold/80">
|
||||||
|
{mediaKind === "video"
|
||||||
|
? t("saveAlbumGuideVideoLabel")
|
||||||
|
: t("saveAlbumGuideImageLabel")}
|
||||||
|
</p>
|
||||||
|
<h2
|
||||||
|
id="save-album-guide-title"
|
||||||
|
className="mt-1 text-lg font-semibold text-white"
|
||||||
|
>
|
||||||
|
{t("saveAlbumGuideTitle")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/10 text-white transition hover:bg-white/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80"
|
||||||
|
aria-label={t("cancel")}
|
||||||
|
>
|
||||||
|
<X className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-5 py-5">
|
||||||
|
<p className="text-sm leading-6 text-neutral-300">
|
||||||
|
{t("saveAlbumGuideIntro")}
|
||||||
|
</p>
|
||||||
|
<ol className="mt-4 space-y-3">
|
||||||
|
{platformStepKeys(platform).map((key, index) => (
|
||||||
|
<li key={key} className="flex gap-3">
|
||||||
|
<span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-ark-gold text-sm font-bold text-black">
|
||||||
|
{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className="pt-0.5 text-sm leading-6 text-neutral-100">
|
||||||
|
{t(key)}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={close}
|
||||||
|
className="mt-5 flex h-11 w-full items-center justify-center rounded-full bg-ark-gold px-4 text-sm font-semibold text-black transition hover:bg-ark-gold2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ark-gold/80 focus-visible:ring-offset-2 focus-visible:ring-offset-[#1c1c21]"
|
||||||
|
>
|
||||||
|
{t("confirm")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body,
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</SaveToAlbumGuideContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,8 +3,12 @@ import { DownloadCloudIcon } from "../icons/DownloadCloudIcon";
|
|||||||
import { useState, type MouseEvent } from "react";
|
import { useState, type MouseEvent } from "react";
|
||||||
import { useI18n } from "../../i18n";
|
import { useI18n } from "../../i18n";
|
||||||
import type { Attachment } from "../../types/post";
|
import type { Attachment } from "../../types/post";
|
||||||
import { downloadAttachment } from "./utils/downloadFile";
|
import { downloadAttachment, pauseActiveVideos } from "./utils/downloadFile";
|
||||||
import { formatBytes } from "./utils/formatBytes";
|
import { formatBytes } from "./utils/formatBytes";
|
||||||
|
import {
|
||||||
|
mediaSaveKindFromAttachment,
|
||||||
|
useSaveToAlbumGuide,
|
||||||
|
} from "../SaveToAlbumGuide";
|
||||||
import { useToast } from "../Toast";
|
import { useToast } from "../Toast";
|
||||||
|
|
||||||
type AttachmentDownloadPillProps = {
|
type AttachmentDownloadPillProps = {
|
||||||
@@ -40,14 +44,18 @@ export function AttachmentDownloadPill({
|
|||||||
}: AttachmentDownloadPillProps) {
|
}: AttachmentDownloadPillProps) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
|
||||||
const handleDownload = async (e: MouseEvent<HTMLButtonElement>) => {
|
const handleDownload = async (e: MouseEvent<HTMLButtonElement>) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (isDownloading) return;
|
if (isDownloading) return;
|
||||||
|
pauseActiveVideos();
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
try {
|
try {
|
||||||
await downloadAttachment(postId, attachment.id, attachment.filename);
|
await downloadAttachment(postId, attachment.id, attachment.filename);
|
||||||
|
const mediaKind = mediaSaveKindFromAttachment(attachment);
|
||||||
|
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
||||||
} catch {
|
} catch {
|
||||||
showToast(t("downloadFail"), "error");
|
showToast(t("downloadFail"), "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -84,6 +92,7 @@ export function AttachmentDownloadPill({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={isDownloading}
|
disabled={isDownloading}
|
||||||
className={`group z-10 inline-flex overflow-hidden rounded-full bg-black/80 ${fontCls} text-white shadow-lg ring-1 ring-inset ring-white/20 backdrop-blur-md transition hover:bg-black/90 disabled:cursor-wait ${className}`}
|
className={`group z-10 inline-flex overflow-hidden rounded-full bg-black/80 ${fontCls} text-white shadow-lg ring-1 ring-inset ring-white/20 backdrop-blur-md transition hover:bg-black/90 disabled:cursor-wait ${className}`}
|
||||||
|
|||||||
@@ -9,11 +9,16 @@ import { filenameWithExtension, splitFilename } from "../utils/filenameDisplay";
|
|||||||
import { formatBytes } from "../utils/formatBytes";
|
import { formatBytes } from "../utils/formatBytes";
|
||||||
import { postDisplayText } from "../utils/postText";
|
import { postDisplayText } from "../utils/postText";
|
||||||
import { CollapsibleText } from "../CollapsibleText";
|
import { CollapsibleText } from "../CollapsibleText";
|
||||||
|
import {
|
||||||
|
mediaSaveKindFromAttachment,
|
||||||
|
useSaveToAlbumGuide,
|
||||||
|
} from "../../SaveToAlbumGuide";
|
||||||
import { useToast } from "../../Toast";
|
import { useToast } from "../../Toast";
|
||||||
|
|
||||||
function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
|
function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
||||||
const { Icon, color } = fileIcon({ mime: att.mime, filename: att.filename });
|
const { Icon, color } = fileIcon({ mime: att.mime, filename: att.filename });
|
||||||
const displayFilename = filenameWithExtension(att.filename, att.mime);
|
const displayFilename = filenameWithExtension(att.filename, att.mime);
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
@@ -24,6 +29,8 @@ function AttachmentRow({ postId, att }: { postId: string; att: Attachment }) {
|
|||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
try {
|
try {
|
||||||
await downloadAttachment(postId, att.id, displayFilename);
|
await downloadAttachment(postId, att.id, displayFilename);
|
||||||
|
const mediaKind = mediaSaveKindFromAttachment(att);
|
||||||
|
if (mediaKind) showSaveToAlbumGuide(mediaKind);
|
||||||
} catch {
|
} catch {
|
||||||
showToast(t("downloadFail"), "error");
|
showToast(t("downloadFail"), "error");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import {
|
|||||||
import { useVideoPlayer } from "../overlays/VideoPlayer";
|
import { useVideoPlayer } from "../overlays/VideoPlayer";
|
||||||
import { autolink } from "../utils/autolink";
|
import { autolink } from "../utils/autolink";
|
||||||
import { CollapsibleText } from "../CollapsibleText";
|
import { CollapsibleText } from "../CollapsibleText";
|
||||||
import { downloadAttachment } from "../utils/downloadFile";
|
import { downloadAttachment, pauseActiveVideos } from "../utils/downloadFile";
|
||||||
import { formatBytes } from "../utils/formatBytes";
|
import { formatBytes } from "../utils/formatBytes";
|
||||||
import { postDisplayText } from "../utils/postText";
|
import { postDisplayText } from "../utils/postText";
|
||||||
import {
|
import {
|
||||||
videoMetadataPreviewSource,
|
videoMetadataPreviewSource,
|
||||||
videoPreviewSource,
|
videoPreviewSource,
|
||||||
} from "../utils/videoPreviewSource";
|
} from "../utils/videoPreviewSource";
|
||||||
|
import { useSaveToAlbumGuide } from "../../SaveToAlbumGuide";
|
||||||
import { useToast } from "../../Toast";
|
import { useToast } from "../../Toast";
|
||||||
|
|
||||||
const MAX_VISIBLE = 4;
|
const MAX_VISIBLE = 4;
|
||||||
@@ -167,13 +168,16 @@ function AttachmentListDownloadButton({
|
|||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
|
const { showSaveToAlbumGuide } = useSaveToAlbumGuide();
|
||||||
const [isDownloading, setIsDownloading] = useState(false);
|
const [isDownloading, setIsDownloading] = useState(false);
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
if (isDownloading) return;
|
if (isDownloading) return;
|
||||||
|
pauseActiveVideos();
|
||||||
setIsDownloading(true);
|
setIsDownloading(true);
|
||||||
try {
|
try {
|
||||||
await downloadAttachment(postId, attachment.id, attachment.filename);
|
await downloadAttachment(postId, attachment.id, attachment.filename);
|
||||||
|
showSaveToAlbumGuide("video");
|
||||||
} catch {
|
} catch {
|
||||||
showToast(t("downloadFail"), "error");
|
showToast(t("downloadFail"), "error");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -184,6 +188,7 @@ function AttachmentListDownloadButton({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleDownload();
|
handleDownload();
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { assetUrl } from "../../../api";
|
import { assetUrl } from "../../../api";
|
||||||
|
|
||||||
|
export function pauseActiveVideos() {
|
||||||
|
document.querySelectorAll("video").forEach((video) => {
|
||||||
|
video.pause();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function attachmentDownloadUrl(postId: string, attachmentId: string) {
|
export function attachmentDownloadUrl(postId: string, attachmentId: string) {
|
||||||
return assetUrl(
|
return assetUrl(
|
||||||
`/api/posts/${encodeURIComponent(postId)}/attachments/${encodeURIComponent(
|
`/api/posts/${encodeURIComponent(postId)}/attachments/${encodeURIComponent(
|
||||||
|
|||||||
@@ -626,13 +626,6 @@ export function PublicLayout() {
|
|||||||
>
|
>
|
||||||
{t("latest")}
|
{t("latest")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
to={lp("/favorites")}
|
|
||||||
className={navClassName(na("favorites"))}
|
|
||||||
aria-current={na("favorites") ? "page" : undefined}
|
|
||||||
>
|
|
||||||
{t("favorites")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to={popularHref}
|
to={popularHref}
|
||||||
className={navClassName(na("browsePopular"))}
|
className={navClassName(na("browsePopular"))}
|
||||||
@@ -717,14 +710,6 @@ export function PublicLayout() {
|
|||||||
>
|
>
|
||||||
{t("latest")}
|
{t("latest")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
|
||||||
to={lp("/favorites")}
|
|
||||||
className={mobileMenuNavClassName(na("favorites"))}
|
|
||||||
aria-current={na("favorites") ? "page" : undefined}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
{t("favorites")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
<Link
|
||||||
to={popularHref}
|
to={popularHref}
|
||||||
className={mobileMenuNavClassName(na("browsePopular"))}
|
className={mobileMenuNavClassName(na("browsePopular"))}
|
||||||
@@ -784,7 +769,7 @@ export function PublicLayout() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<nav className="fixed inset-x-0 bottom-0 z-40 select-none bg-[#0C0D0F]/95 pb-[max(env(safe-area-inset-bottom),0px)] backdrop-blur md:hidden">
|
<nav className="fixed inset-x-0 bottom-0 z-40 select-none bg-[#0C0D0F]/95 pb-[max(env(safe-area-inset-bottom),0px)] backdrop-blur md:hidden">
|
||||||
<div className="grid h-[68px] grid-cols-4 gap-3 px-5 py-[10px] text-center text-[11px] leading-[17.6px]">
|
<div className="grid h-[68px] grid-cols-3 gap-3 px-5 py-[10px] text-center text-[11px] leading-[17.6px]">
|
||||||
<BottomNavIcon
|
<BottomNavIcon
|
||||||
to={homePath}
|
to={homePath}
|
||||||
label={t("home")}
|
label={t("home")}
|
||||||
@@ -800,12 +785,6 @@ export function PublicLayout() {
|
|||||||
!new URLSearchParams(search).get("sort")
|
!new URLSearchParams(search).get("sort")
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<BottomNavIcon
|
|
||||||
to={lp("/favorites")}
|
|
||||||
label={t("favorites")}
|
|
||||||
icon="bookmark"
|
|
||||||
active={na("favorites")}
|
|
||||||
/>
|
|
||||||
<BottomNavIcon
|
<BottomNavIcon
|
||||||
to={popularHref}
|
to={popularHref}
|
||||||
label={t("popular")}
|
label={t("popular")}
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ export const enDict: Dict = {
|
|||||||
downloadOk: "Download complete",
|
downloadOk: "Download complete",
|
||||||
downloadFail: "Download failed, please retry",
|
downloadFail: "Download failed, please retry",
|
||||||
longPressImageSave: "Long-press image to save",
|
longPressImageSave: "Long-press image to save",
|
||||||
|
saveAlbumGuideTitle: "Save to album guide",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"Your download has started. Follow the steps below to save the file to your phone album.",
|
||||||
|
saveAlbumGuideImageLabel: "Image",
|
||||||
|
saveAlbumGuideVideoLabel: "Video",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"After the download finishes, tap Safari’s download icon or open the Downloads folder in the Files app.",
|
||||||
|
saveAlbumGuideIosStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"Tap the share button and choose Save Image or Save Video to add it to Photos.",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"After the download finishes, tap the download notification or open Files / Downloads.",
|
||||||
|
saveAlbumGuideAndroidStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"If it does not appear in Gallery automatically, use the menu to save or move it to Photos/Gallery.",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"After the download finishes, open it from your browser downloads list or the Downloads folder.",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"To keep it in a phone album, transfer it to your phone with AirDrop, cable, or cloud drive, then save it there.",
|
||||||
showMore: "Show all",
|
showMore: "Show all",
|
||||||
showLess: "Show less",
|
showLess: "Show less",
|
||||||
share: "Share",
|
share: "Share",
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ export const idDict: Dict = {
|
|||||||
downloadOk: "Unduhan selesai",
|
downloadOk: "Unduhan selesai",
|
||||||
downloadFail: "Unduhan gagal, silakan coba lagi",
|
downloadFail: "Unduhan gagal, silakan coba lagi",
|
||||||
longPressImageSave: "Tekan lama gambar untuk menyimpan",
|
longPressImageSave: "Tekan lama gambar untuk menyimpan",
|
||||||
|
saveAlbumGuideTitle: "Save to album guide",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"Your download has started. Follow the steps below to save the file to your phone album.",
|
||||||
|
saveAlbumGuideImageLabel: "Image",
|
||||||
|
saveAlbumGuideVideoLabel: "Video",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"After the download finishes, tap Safari’s download icon or open the Downloads folder in the Files app.",
|
||||||
|
saveAlbumGuideIosStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"Tap the share button and choose Save Image or Save Video to add it to Photos.",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"After the download finishes, tap the download notification or open Files / Downloads.",
|
||||||
|
saveAlbumGuideAndroidStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"If it does not appear in Gallery automatically, use the menu to save or move it to Photos/Gallery.",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"After the download finishes, open it from your browser downloads list or the Downloads folder.",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"To keep it in a phone album, transfer it to your phone with AirDrop, cable, or cloud drive, then save it there.",
|
||||||
showMore: "Lihat semua",
|
showMore: "Lihat semua",
|
||||||
showLess: "Tutup",
|
showLess: "Tutup",
|
||||||
share: "Bagikan",
|
share: "Bagikan",
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ export const jaDict: Dict = {
|
|||||||
downloadOk: "ダウンロード完了",
|
downloadOk: "ダウンロード完了",
|
||||||
downloadFail: "ダウンロードに失敗しました。再試行してください",
|
downloadFail: "ダウンロードに失敗しました。再試行してください",
|
||||||
longPressImageSave: "画像を長押しして保存",
|
longPressImageSave: "画像を長押しして保存",
|
||||||
|
saveAlbumGuideTitle: "Save to album guide",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"Your download has started. Follow the steps below to save the file to your phone album.",
|
||||||
|
saveAlbumGuideImageLabel: "Image",
|
||||||
|
saveAlbumGuideVideoLabel: "Video",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"After the download finishes, tap Safari’s download icon or open the Downloads folder in the Files app.",
|
||||||
|
saveAlbumGuideIosStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"Tap the share button and choose Save Image or Save Video to add it to Photos.",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"After the download finishes, tap the download notification or open Files / Downloads.",
|
||||||
|
saveAlbumGuideAndroidStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"If it does not appear in Gallery automatically, use the menu to save or move it to Photos/Gallery.",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"After the download finishes, open it from your browser downloads list or the Downloads folder.",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"To keep it in a phone album, transfer it to your phone with AirDrop, cable, or cloud drive, then save it there.",
|
||||||
showMore: "すべて表示",
|
showMore: "すべて表示",
|
||||||
showLess: "閉じる",
|
showLess: "閉じる",
|
||||||
share: "シェア",
|
share: "シェア",
|
||||||
|
|||||||
@@ -36,6 +36,25 @@ export const koDict: Dict = {
|
|||||||
downloadOk: "다운로드 완료",
|
downloadOk: "다운로드 완료",
|
||||||
downloadFail: "다운로드 실패, 다시 시도해 주세요",
|
downloadFail: "다운로드 실패, 다시 시도해 주세요",
|
||||||
longPressImageSave: "이미지를 길게 눌러 저장",
|
longPressImageSave: "이미지를 길게 눌러 저장",
|
||||||
|
saveAlbumGuideTitle: "Save to album guide",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"Your download has started. Follow the steps below to save the file to your phone album.",
|
||||||
|
saveAlbumGuideImageLabel: "Image",
|
||||||
|
saveAlbumGuideVideoLabel: "Video",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"After the download finishes, tap Safari’s download icon or open the Downloads folder in the Files app.",
|
||||||
|
saveAlbumGuideIosStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"Tap the share button and choose Save Image or Save Video to add it to Photos.",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"After the download finishes, tap the download notification or open Files / Downloads.",
|
||||||
|
saveAlbumGuideAndroidStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"If it does not appear in Gallery automatically, use the menu to save or move it to Photos/Gallery.",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"After the download finishes, open it from your browser downloads list or the Downloads folder.",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"To keep it in a phone album, transfer it to your phone with AirDrop, cable, or cloud drive, then save it there.",
|
||||||
showMore: "모두 보기",
|
showMore: "모두 보기",
|
||||||
showLess: "접기",
|
showLess: "접기",
|
||||||
share: "공유",
|
share: "공유",
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ export const msDict: Dict = {
|
|||||||
downloadOk: "Muat turun selesai",
|
downloadOk: "Muat turun selesai",
|
||||||
downloadFail: "Muat turun gagal, sila cuba lagi",
|
downloadFail: "Muat turun gagal, sila cuba lagi",
|
||||||
longPressImageSave: "Tekan lama imej untuk simpan",
|
longPressImageSave: "Tekan lama imej untuk simpan",
|
||||||
|
saveAlbumGuideTitle: "Save to album guide",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"Your download has started. Follow the steps below to save the file to your phone album.",
|
||||||
|
saveAlbumGuideImageLabel: "Image",
|
||||||
|
saveAlbumGuideVideoLabel: "Video",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"After the download finishes, tap Safari’s download icon or open the Downloads folder in the Files app.",
|
||||||
|
saveAlbumGuideIosStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"Tap the share button and choose Save Image or Save Video to add it to Photos.",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"After the download finishes, tap the download notification or open Files / Downloads.",
|
||||||
|
saveAlbumGuideAndroidStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"If it does not appear in Gallery automatically, use the menu to save or move it to Photos/Gallery.",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"After the download finishes, open it from your browser downloads list or the Downloads folder.",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"To keep it in a phone album, transfer it to your phone with AirDrop, cable, or cloud drive, then save it there.",
|
||||||
showMore: "Lihat semua",
|
showMore: "Lihat semua",
|
||||||
showLess: "Tutup",
|
showLess: "Tutup",
|
||||||
share: "Kongsi",
|
share: "Kongsi",
|
||||||
|
|||||||
@@ -37,6 +37,25 @@ export const viDict: Dict = {
|
|||||||
downloadOk: "Tải xuống hoàn tất",
|
downloadOk: "Tải xuống hoàn tất",
|
||||||
downloadFail: "Tải xuống thất bại, vui lòng thử lại",
|
downloadFail: "Tải xuống thất bại, vui lòng thử lại",
|
||||||
longPressImageSave: "Nhấn giữ ảnh để lưu",
|
longPressImageSave: "Nhấn giữ ảnh để lưu",
|
||||||
|
saveAlbumGuideTitle: "Save to album guide",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"Your download has started. Follow the steps below to save the file to your phone album.",
|
||||||
|
saveAlbumGuideImageLabel: "Image",
|
||||||
|
saveAlbumGuideVideoLabel: "Video",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"After the download finishes, tap Safari’s download icon or open the Downloads folder in the Files app.",
|
||||||
|
saveAlbumGuideIosStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"Tap the share button and choose Save Image or Save Video to add it to Photos.",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"After the download finishes, tap the download notification or open Files / Downloads.",
|
||||||
|
saveAlbumGuideAndroidStep2: "Open the downloaded image or video.",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"If it does not appear in Gallery automatically, use the menu to save or move it to Photos/Gallery.",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"After the download finishes, open it from your browser downloads list or the Downloads folder.",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"To keep it in a phone album, transfer it to your phone with AirDrop, cable, or cloud drive, then save it there.",
|
||||||
showMore: "Xem tất cả",
|
showMore: "Xem tất cả",
|
||||||
showLess: "Thu gọn",
|
showLess: "Thu gọn",
|
||||||
share: "Chia sẻ",
|
share: "Chia sẻ",
|
||||||
|
|||||||
@@ -36,6 +36,25 @@ export const zhDict: Dict = {
|
|||||||
downloadOk: "下载完成",
|
downloadOk: "下载完成",
|
||||||
downloadFail: "下载失败,请重试",
|
downloadFail: "下载失败,请重试",
|
||||||
longPressImageSave: "长按图片保存到相册",
|
longPressImageSave: "长按图片保存到相册",
|
||||||
|
saveAlbumGuideTitle: "保存到相册指南",
|
||||||
|
saveAlbumGuideIntro:
|
||||||
|
"下载已经开始。不同手机系统需要按下面步骤把文件保存到相册。",
|
||||||
|
saveAlbumGuideImageLabel: "图片",
|
||||||
|
saveAlbumGuideVideoLabel: "视频",
|
||||||
|
saveAlbumGuideIosStep1:
|
||||||
|
"下载完成后,点 Safari 地址栏旁边的下载图标,或打开「文件」App 的「下载」文件夹。",
|
||||||
|
saveAlbumGuideIosStep2: "打开刚下载的图片或视频。",
|
||||||
|
saveAlbumGuideIosStep3:
|
||||||
|
"点左下角分享按钮,选择「储存图像」或「储存视频」,即可保存到照片。",
|
||||||
|
saveAlbumGuideAndroidStep1:
|
||||||
|
"下载完成后,点系统下载通知,或打开「文件管理 / Downloads」文件夹。",
|
||||||
|
saveAlbumGuideAndroidStep2: "打开刚下载的图片或视频。",
|
||||||
|
saveAlbumGuideAndroidStep3:
|
||||||
|
"如果没有自动出现在相册,请点更多菜单,选择保存到相册/图库。",
|
||||||
|
saveAlbumGuideDesktopStep1:
|
||||||
|
"下载完成后,在浏览器的下载列表或电脑 Downloads 文件夹中打开文件。",
|
||||||
|
saveAlbumGuideDesktopStep2:
|
||||||
|
"如果需要同步到手机相册,请通过 AirDrop、数据线或云盘传到手机后保存。",
|
||||||
showMore: "展开全部",
|
showMore: "展开全部",
|
||||||
showLess: "收起全部",
|
showLess: "收起全部",
|
||||||
share: "分享",
|
share: "分享",
|
||||||
|
|||||||
Reference in New Issue
Block a user