- Updated .env.example to include configuration for OnlyOffice Document Server. - Modified the workspace configuration to remove the drive-suite path. - Adjusted TypeScript environment imports for consistency. - Enhanced Next.js configuration to disable canvas in Webpack. - Updated package.json to include new dependencies for OnlyOffice and PDF.js. - Added global styles for OnlyOffice theme integration in the CSS. - Created new layout and page components for the Drive feature, including public sharing and editing functionalities. - Updated metadata handling across various layouts to reflect the new app structure.
233 lines
6.9 KiB
TypeScript
233 lines
6.9 KiB
TypeScript
import type { DriveFileInfo } from "@/lib/api/types"
|
|
import { parentFolderPath } from "@/lib/drive/drive-search"
|
|
import { normalizeDriveFolderPath } from "@/lib/drive/drive-sidebar-tree"
|
|
import type { DriveFiltersSnapshot } from "@/lib/stores/drive-filters-store"
|
|
import { driveFiltersActive } from "@/lib/stores/drive-filters-store"
|
|
|
|
export type DriveMimeCategory =
|
|
| "folder"
|
|
| "document"
|
|
| "spreadsheet"
|
|
| "presentation"
|
|
| "image"
|
|
| "pdf"
|
|
| "video"
|
|
| "audio"
|
|
| "archive"
|
|
| "other"
|
|
|
|
export type DriveSourceId = "ultimail" | "ultimeet"
|
|
|
|
const ARCHIVE_EXT = new Set(["zip", "rar", "7z", "tar", "gz", "bz2"])
|
|
const AUDIO_EXT = new Set(["mp3", "wav", "ogg", "flac", "m4a", "aac"])
|
|
const IMAGE_EXT = new Set(["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "avif", "heic"])
|
|
const VIDEO_EXT = new Set(["mp4", "webm", "mov", "mkv", "ogv", "m4v"])
|
|
|
|
function ext(name: string) {
|
|
const i = name.lastIndexOf(".")
|
|
return i > 0 ? name.slice(i + 1).toLowerCase() : ""
|
|
}
|
|
|
|
export function classifyDriveFile(file: DriveFileInfo): DriveMimeCategory {
|
|
if (file.type === "directory") return "folder"
|
|
const mime = (file.mime_type ?? "").toLowerCase()
|
|
const e = ext(file.name)
|
|
if (mime.includes("pdf") || e === "pdf") return "pdf"
|
|
if (mime.startsWith("image/") || IMAGE_EXT.has(e)) return "image"
|
|
if (mime.startsWith("video/") || VIDEO_EXT.has(e)) return "video"
|
|
if (mime.startsWith("audio/") || AUDIO_EXT.has(e)) return "audio"
|
|
if (
|
|
mime.includes("zip") ||
|
|
mime.includes("archive") ||
|
|
ARCHIVE_EXT.has(e)
|
|
) {
|
|
return "archive"
|
|
}
|
|
if (
|
|
mime.includes("spreadsheet") ||
|
|
mime.includes("excel") ||
|
|
["xls", "xlsx", "ods", "csv"].includes(e)
|
|
) {
|
|
return "spreadsheet"
|
|
}
|
|
if (
|
|
mime.includes("presentation") ||
|
|
mime.includes("powerpoint") ||
|
|
["ppt", "pptx", "odp"].includes(e)
|
|
) {
|
|
return "presentation"
|
|
}
|
|
if (
|
|
mime.includes("word") ||
|
|
mime.includes("document") ||
|
|
mime.includes("opendocument.text") ||
|
|
["doc", "docx", "odt", "rtf", "txt"].includes(e)
|
|
) {
|
|
return "document"
|
|
}
|
|
return "other"
|
|
}
|
|
|
|
export function matchesDriveSource(file: DriveFileInfo, source: DriveSourceId): boolean {
|
|
if (file.source === source) return true
|
|
|
|
const p = file.path.toLowerCase()
|
|
const n = file.name.toLowerCase()
|
|
if (source === "ultimail") {
|
|
return (
|
|
p.includes("/ultimail") ||
|
|
p.includes("/mail/") ||
|
|
n.includes("ultimail") ||
|
|
(file.mime_type ?? "").includes("rfc822")
|
|
)
|
|
}
|
|
return (
|
|
file.source === "ultimeet" ||
|
|
p.includes("/ultimeet") ||
|
|
p.includes("/meet") ||
|
|
p.includes("/recordings") ||
|
|
n.includes("ultimeet") ||
|
|
n.includes("meet-")
|
|
)
|
|
}
|
|
|
|
function parseModified(iso: string): Date | null {
|
|
const d = new Date(iso)
|
|
return Number.isNaN(d.getTime()) ? null : d
|
|
}
|
|
|
|
function inDateRange(file: DriveFileInfo, filters: DriveFiltersSnapshot): boolean {
|
|
const { datePreset, dateFrom, dateTo } = filters
|
|
if (!datePreset) return true
|
|
const modified = parseModified(file.last_modified)
|
|
if (!modified) return true
|
|
|
|
const now = new Date()
|
|
const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
|
|
|
if (datePreset === "today") {
|
|
const today = startOfDay(now)
|
|
return modified >= today
|
|
}
|
|
if (datePreset === "last7") {
|
|
const from = new Date(now)
|
|
from.setDate(from.getDate() - 7)
|
|
return modified >= from
|
|
}
|
|
if (datePreset === "last30") {
|
|
const from = new Date(now)
|
|
from.setDate(from.getDate() - 30)
|
|
return modified >= from
|
|
}
|
|
if (datePreset === "thisYear") {
|
|
return modified.getFullYear() === now.getFullYear()
|
|
}
|
|
if (datePreset === "lastYear") {
|
|
return modified.getFullYear() === now.getFullYear() - 1
|
|
}
|
|
if (datePreset === "custom" && dateFrom) {
|
|
const from = startOfDay(new Date(dateFrom))
|
|
const to = dateTo ? new Date(dateTo) : now
|
|
return modified >= from && modified <= to
|
|
}
|
|
return true
|
|
}
|
|
|
|
export function matchesDriveFilters(
|
|
file: DriveFileInfo,
|
|
filters: DriveFiltersSnapshot
|
|
): boolean {
|
|
if (filters.types.size > 0) {
|
|
const cat = classifyDriveFile(file)
|
|
if (!filters.types.has(cat)) return false
|
|
}
|
|
if (filters.sources.size > 0) {
|
|
const ok = [...filters.sources].some((s) => matchesDriveSource(file, s))
|
|
if (!ok) return false
|
|
}
|
|
if (!matchesContact(file, filters)) return false
|
|
if (!inDateRange(file, filters)) return false
|
|
return true
|
|
}
|
|
|
|
function isPathUnderScope(path: string, scopePath: string): boolean {
|
|
const normalized = normalizeDriveFolderPath(path)
|
|
const scope = normalizeDriveFolderPath(scopePath)
|
|
if (scope === "/") return true
|
|
return normalized === scope || normalized.startsWith(`${scope}/`)
|
|
}
|
|
|
|
/** Folder paths that contain (or nest) at least one file matching filters. */
|
|
export function buildDriveFolderPathsWithMatches(
|
|
corpus: DriveFileInfo[],
|
|
filters: DriveFiltersSnapshot,
|
|
scopePath = "/"
|
|
): Set<string> {
|
|
const keep = new Set<string>()
|
|
const scope = normalizeDriveFolderPath(scopePath)
|
|
|
|
for (const file of corpus) {
|
|
if (file.type === "directory") continue
|
|
if (!matchesDriveFilters(file, filters)) continue
|
|
|
|
let dir = parentFolderPath(file.path)
|
|
while (dir !== "/" && isPathUnderScope(dir, scope)) {
|
|
const normalized = normalizeDriveFolderPath(dir)
|
|
keep.add(normalized)
|
|
if (normalized === scope) break
|
|
dir = parentFolderPath(normalized)
|
|
}
|
|
}
|
|
|
|
return keep
|
|
}
|
|
|
|
function matchesContact(file: DriveFileInfo, filters: DriveFiltersSnapshot): boolean {
|
|
if (!filters.contactEmail && !filters.contactName) return true
|
|
const hay = `${file.path} ${file.name}`.toLowerCase()
|
|
if (filters.contactEmail && hay.includes(filters.contactEmail.toLowerCase())) {
|
|
return true
|
|
}
|
|
if (filters.contactName) {
|
|
const parts = filters.contactName.toLowerCase().split(/\s+/).filter(Boolean)
|
|
if (parts.length > 0 && parts.every((p) => hay.includes(p))) return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
export function applyDriveFilters(
|
|
items: DriveFileInfo[],
|
|
filters: DriveFiltersSnapshot,
|
|
options?: {
|
|
/** When set, only folders in this set stay visible (plus direct filter matches). */
|
|
folderKeepPaths?: Set<string>
|
|
/** Corpus for folder pruning when folderKeepPaths is omitted. */
|
|
matchCorpus?: DriveFileInfo[]
|
|
/** Limit folder pruning to descendants of this path (default `/`). */
|
|
scopePath?: string
|
|
}
|
|
): DriveFileInfo[] {
|
|
if (!driveFiltersActive(filters)) return items
|
|
|
|
const folderKeepPaths =
|
|
options?.folderKeepPaths ??
|
|
(options?.matchCorpus
|
|
? buildDriveFolderPathsWithMatches(
|
|
options.matchCorpus,
|
|
filters,
|
|
options.scopePath ?? "/"
|
|
)
|
|
: undefined)
|
|
|
|
return items.filter((file) => {
|
|
if (file.type === "directory") {
|
|
const path = normalizeDriveFolderPath(file.path)
|
|
if (folderKeepPaths?.has(path)) return true
|
|
return matchesDriveFilters(file, filters)
|
|
}
|
|
return matchesDriveFilters(file, filters)
|
|
})
|
|
}
|
|
|
|
export { driveFiltersActive as hasActiveDriveFilters }
|