UNPKG

@eternalheart/ngx-file-preview

Version:

A powerful Angular file preview component library supporting multiple file formats including images, videos, PDFs, Office documents, text files and more.

1 lines 220 kB
{"version":3,"file":"eternalheart-ngx-file-preview.mjs","sources":["../../../libs/ngx-file-preview/src/lib/services/theme.service.ts","../../../libs/ngx-file-preview/src/lib/i18n/i18n.utils.ts","../../../libs/ngx-file-preview/src/lib/services/preview.service.ts","../../../libs/ngx-file-preview/src/lib/services/file-reader.service.ts","../../../libs/ngx-file-preview/src/lib/preview-types/base-preview/base-preview.component.ts","../../../libs/ngx-file-preview/src/lib/directives/tooltip.diretive.ts","../../../libs/ngx-file-preview/src/lib/components/preview-icon/preview-icon.component.ts","../../../libs/ngx-file-preview/src/lib/i18n/i18n.pipe.ts","../../../libs/ngx-file-preview/src/lib/preview-types/excel-preview/excel-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/archive-preview/archive-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/word-preview/word-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/pdf-preview/pdf-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/image-preview/image-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/video-preview/video-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/ppt-preview/ppt-preview.component.ts","../../../libs/ngx-file-preview/src/lib/utils/preview.utils.ts","../../../libs/ngx-file-preview/src/lib/directives/preview.directive.ts","../../../libs/ngx-file-preview/src/lib/preview-types/text-preview/text-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/audio-preview/audio-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/unknown-preview/unknown-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/markdown-preview/markdown.pipe.ts","../../../libs/ngx-file-preview/src/lib/preview-types/markdown-preview/markdown-preview.component.ts","../../../libs/ngx-file-preview/src/lib/preview-types/markdown-preview/markdown-preview.component.html","../../../libs/ngx-file-preview/src/lib/components/theme-icon/theme-icon.component.ts","../../../libs/ngx-file-preview/src/lib/components/theme-icon/theme-icon.component.html","../../../libs/ngx-file-preview/src/lib/components/preview-modal/preview-modal.component.ts","../../../libs/ngx-file-preview/src/lib/components/preview-modal/preview-modal.component.html","../../../libs/ngx-file-preview/src/lib/components/index.ts","../../../libs/ngx-file-preview/src/lib/preview-list/file-size.pipe.ts","../../../libs/ngx-file-preview/src/lib/preview-list/preview-list.component.ts","../../../libs/ngx-file-preview/src/lib/preview-list/preview-list.component.html","../../../libs/ngx-file-preview/src/version.ts","../../../libs/ngx-file-preview/src/index.ts","../../../libs/ngx-file-preview/src/eternalheart-ngx-file-preview.ts"],"sourcesContent":["import {Injectable, Renderer2} from '@angular/core';\nimport {BehaviorSubject} from 'rxjs';\nimport {AutoThemeConfig, ThemeMode} from '../types/theme.types';\n\n@Injectable()\nexport class ThemeService {\n private readonly THEME_KEY = 'fp-theme-mode';\n private themeSubject$ = new BehaviorSubject<ThemeMode>('dark');\n private autoConfig: AutoThemeConfig = {\n dark: {start: 18, end: 6}\n };\n\n private autoChangeTimer: any;\n systemThemeQuery: MediaQueryList | null = null;\n private systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null;\n private localDomElement: HTMLElement | null = null;\n constructor(private renderer: Renderer2) {\n }\n\n /**\n * 绑定最外围元素\n * @param domElement\n */\n bindElement(domElement: HTMLElement) {\n this.localDomElement = domElement;\n }\n\n ngOnInit() {\n if (window.matchMedia) {\n this.systemThemeQuery = window.matchMedia('(prefers-color-scheme: dark)');\n this.systemThemeListener = (e) => {\n if (this.theme === 'auto') {\n this.checkAndApplyAutoTheme();\n }\n };\n this.systemThemeQuery.addEventListener('change', this.systemThemeListener);\n }\n this.applyTheme('dark');\n }\n\n get theme() {\n return this.themeSubject$.getValue();\n }\n\n getThemeObservable() {\n return this.themeSubject$.asObservable();\n }\n\n setMode(mode: ThemeMode) {\n this.themeSubject$.next(mode);\n if (mode === 'auto') {\n this.startAutoCheck();\n } else {\n this.stopAutoCheck();\n this.applyTheme(mode);\n }\n }\n\n setAutoConfig(config: AutoThemeConfig) {\n this.autoConfig = {...this.autoConfig, ...config};\n if (this.themeSubject$.getValue() === 'auto') {\n this.checkAndApplyAutoTheme();\n }\n }\n\n private startAutoCheck() {\n this.checkAndApplyAutoTheme();\n this.autoChangeTimer = setInterval(() => {\n this.checkAndApplyAutoTheme();\n }, 60000); // 每分钟检查一次\n }\n\n private stopAutoCheck() {\n if (this.autoChangeTimer) {\n clearInterval(this.autoChangeTimer);\n this.autoChangeTimer = null;\n }\n }\n\n private checkAndApplyAutoTheme() {\n const hour = new Date().getHours();\n const {start, end} = this.autoConfig.dark;\n\n // 检查是否在暗色时间范围内\n const isDarkTime = start > end\n ? (hour >= start || hour < end) // 跨夜间\n : (hour >= start && hour < end); // 同一天内\n\n // 检查系统主题\n const prefersDark = this.systemThemeQuery?.matches ?? false;\n\n // 优先使用时间判断,其次使用系统主题\n this.applyTheme(isDarkTime || prefersDark ? 'dark' : 'light');\n }\n\n private applyTheme(theme: 'light' | 'dark') {\n // 更新当前主题\n this.themeSubject$.next(theme);\n if(this.localDomElement){\n // 移除现有主题\n this.renderer.removeAttribute(this.localDomElement, 'data-nfp-theme');\n // 应用新主题\n if (theme === 'dark') {\n this.renderer.setAttribute(this.localDomElement, 'data-nfp-theme', 'dark');\n } else {\n this.renderer.setAttribute(this.localDomElement, 'data-nfp-theme', 'light');\n }\n }\n // 保存到本地存储\n localStorage.setItem(this.THEME_KEY, theme);\n }\n\n toggleTheme() {\n const newTheme = this.theme === 'light' ? 'dark' : 'light';\n this.setMode(newTheme);\n }\n\n ngOnDestroy() {\n this.stopAutoCheck();\n\n // 清理系统主题监听\n if (this.systemThemeQuery && this.systemThemeListener) {\n this.systemThemeQuery.removeEventListener('change', this.systemThemeListener);\n }\n }\n}\n","import ZH from \"../../assets/i18n/zh.json\";\nimport EN from \"../../assets/i18n/en.json\";\n\nconst LangMapping: Record<string, any> = {\n 'zh': ZH,\n}\nexport const I18nUtils = {\n /**\n * 获取语言包\n * @param locale\n */\n get(locale: string) {\n return new I18nParser(locale || 'zh')\n },\n /**\n * 注册语言包\n */\n register(locale: string, langJson: typeof ZH) {\n LangMapping[locale] = langJson;\n }\n}\n\n/**\n * 注册使用示例\n */\nI18nUtils.register('en', EN)\n\n/**\n * 单例模式优化语言包的获取 单个语言只会创建一个语言转化实例\n */\nclass I18nParser {\n static InstanceMap: Record<string, I18nParser> = {}\n public locale: string = 'zh';\n\n constructor(locale: string) {\n this.locale = locale\n if (I18nParser.InstanceMap[locale]) return I18nParser.InstanceMap[locale];\n I18nParser.InstanceMap[locale] = this\n }\n // 翻译\n public t(key: string, ...args:(string|number)[]): string {\n const translated = I18nParser.getValue(LangMapping[this.locale], key);\n if(args.length>0) return translated.replace(/\\${(\\d+)}/g, (match:any, index:number) => args[index]);\n if (translated) return translated\n return key;\n }\n\n // 获取深层值\n static getValue(data: Record<string, any>, prop: string | string[]): any {\n let ps = Array.isArray(prop) ? prop : prop.split('.');\n try {\n return ps.length == 1 ? data[ps.shift()!] : I18nParser.getValue(data[ps.shift()!], ps);\n } catch (e) {\n return undefined;\n }\n }\n}\n","import {\n ApplicationRef,\n ComponentRef,\n createComponent,\n EnvironmentInjector,\n inject,\n Injectable,\n Injector\n} from '@angular/core';\nimport {PreviewFile, PreviewOptions} from '../types/preview.types';\nimport {BehaviorSubject} from 'rxjs';\nimport {PreviewModalComponent} from '../components';\nimport {ThemeService} from \"./theme.service\";\nimport {I18nUtils} from \"../i18n/i18n.utils\";\n\nexport const INITIAL_PREVIEW_STATE = {\n isVisible: false,\n currentIndex: 0,\n files: []\n}\n\nexport interface PreviewState {\n isVisible: boolean;\n currentFile?: PreviewFile;\n currentIndex: number;\n files: PreviewFile[];\n}\n\n@Injectable()\nexport class PreviewService {\n // region 服务管理\n /**\n * 初始化 需要将所有service注入到modal\n */\n private injector!: Injector;\n private envInjector!: EnvironmentInjector;\n private appRef = inject(ApplicationRef)\n private lang: string = 'zh';\n private loading: BehaviorSubject<boolean> = new BehaviorSubject(false);\n\n /**\n * 初始化\n * @param injector\n * @param envInjector\n */\n init(injector: Injector, envInjector: EnvironmentInjector) {\n this.envInjector = envInjector;\n this.injector = injector;\n }\n\n /**\n * 设置语言\n * @param lang\n */\n setLang(lang: string) {\n this.lang = lang;\n }\n\n /**\n * 获取实际的lang parser\n */\n getLangParser() {\n return I18nUtils.get(this.lang)\n }\n\n // endregion\n // region 状态管理\n readonly stateSubject = new BehaviorSubject<PreviewState>(INITIAL_PREVIEW_STATE);\n\n get state() {\n return this.stateSubject.getValue();\n }\n\n getStateObservable() {\n return this.stateSubject.asObservable();\n }\n\n previous() {\n const state = this.state;\n const newIndex = Math.max(0, state.currentIndex - 1);\n this.updatePreviewState(true, state.files, newIndex);\n }\n\n next() {\n const state = this.state;\n const newIndex = Math.min(state.files.length - 1, state.currentIndex + 1);\n this.updatePreviewState(true, state.files, newIndex);\n }\n\n private updatePreviewState(isVisible: boolean, files: PreviewFile[], index: number) {\n const currentFile = files[index];\n this.stateSubject.next({\n isVisible,\n currentFile,\n currentIndex: index,\n files\n });\n }\n\n /**\n * 设置加载中状态\n * @param loading\n */\n setLoading(loading: boolean) {\n this.loading.next(loading);\n }\n\n getLoadingObservable() {\n return this.loading.asObservable();\n }\n\n // endregion\n // region Modal管理\n private modalRef?: ComponentRef<PreviewModalComponent>;\n\n get modalElement() {\n return this.modalRef?.location.nativeElement\n }\n\n open(options: PreviewOptions) {\n const {files, index = 0} = options;\n if (this.modalRef) {\n this.cleanupModal()\n }\n try {\n this.modalRef = createComponent(PreviewModalComponent, {\n environmentInjector: this.envInjector,\n elementInjector: this.injector,\n });\n Object.assign(this.modalRef.instance, options)\n this.injector.get(ThemeService).bindElement(this.modalRef.location.nativeElement)\n document.body.appendChild(this.modalRef.location.nativeElement);\n this.modalRef.changeDetectorRef.detectChanges();\n this.updatePreviewState(true, files, index);\n this.appRef.attachView(this.modalRef.hostView);\n } catch (error) {\n console.error('Error creating preview-list modal:', error);\n this.cleanupModal();\n }\n }\n\n close() {\n if (document.fullscreenElement) {\n document?.exitFullscreen();\n }\n this.updatePreviewState(false, [], 0);\n this.cleanupModal();\n }\n\n private cleanupModal() {\n if (!this.modalRef) return;\n try {\n // 从 DOM 中移除模态框\n const element = this.modalRef.location.nativeElement;\n if (element.parentNode) {\n element.parentNode.removeChild(element);\n }\n // 从 ApplicationRef 中分离视图\n this.appRef.detachView(this.modalRef.hostView);\n // 销毁组件\n this.modalRef.destroy();\n } catch (error) {\n console.error('Error cleaning up modal:', error);\n } finally {\n this.modalRef = undefined;\n }\n }\n\n // endregion\n\n\n}\n","import {Injectable} from '@angular/core';\nimport {Observable, Subject, from} from 'rxjs';\nimport {PreviewFile} from \"../types\";\n\nexport interface FileReaderResponse {\n type: 'success' | 'error';\n data?: ArrayBuffer;\n text?: string;\n json?: any;\n error?: string;\n}\n\n@Injectable()\nexport class FileReaderService {\n private responseSubject = new Subject<FileReaderResponse>();\n private async readFileData(file: PreviewFile, fileType: 'arraybuffer' | 'text' | 'json'): Promise<FileReaderResponse> {\n try {\n const response = await fetch(file.url);\n if (!response.ok) {\n throw new Error(`HTTP error! status: ${response.status}`);\n }\n\n const result: FileReaderResponse = { type: 'success' };\n\n switch (fileType) {\n case 'arraybuffer':\n result.data = await response.arrayBuffer();\n break;\n case 'text':\n result.text = await response.text();\n break;\n case 'json':\n const text = await response.text();\n try {\n result.json = JSON.parse(text);\n } catch (parseError: any) {\n throw new Error(`JSON parse failed: ${parseError.message}`);\n }\n break;\n }\n\n return result;\n } catch (error) {\n return {\n type: 'error',\n error: error instanceof Error ? error.message : 'Unknown error'\n };\n }\n }\n\n readFile(file: PreviewFile, fileType: 'arraybuffer' | 'text' | 'json' = 'arraybuffer'): Observable<FileReaderResponse> {\n return from(this.readFileData(file, fileType));\n }\n\n ngOnDestroy() {\n this.responseSubject.complete();\n }\n}\n","import {ChangeDetectorRef, Directive, inject, Input} from '@angular/core';\nimport {AutoThemeConfig, PreviewFile, ThemeMode} from '../../types';\nimport {FileReaderResponse, FileReaderService, PreviewService} from \"../../services\";\nimport {firstValueFrom} from \"rxjs\";\n\n@Directive({\n standalone: true,\n})\nexport abstract class BasePreviewComponent {\n @Input() file!: PreviewFile;\n @Input({transform: (value: ThemeMode | null): ThemeMode => value!}) themeMode!: ThemeMode;\n @Input() autoThemeConfig?: AutoThemeConfig;\n\n protected fileReader = inject(FileReaderService);\n protected previewService = inject(PreviewService);\n protected cdr = inject(ChangeDetectorRef);\n\n get isLoading() {\n return this.previewService.getLoadingObservable();\n }\n\n t(key: string, ...args: (string | number)[]) {\n return this.previewService?.getLangParser()?.t(key, ...args);\n }\n\n protected async loadFile(fileType?: 'arraybuffer' | 'text' | 'json'): Promise<void> {\n if (!this.file) return;\n this.startLoading();\n try {\n const content = await firstValueFrom(this.fileReader.readFile(this.file, fileType));\n await this.handleFileContent(content);\n } catch (error) {\n console.error('Failed to read file:', error);\n } finally {\n this.stopLoading();\n }\n }\n\n protected abstract handleFileContent(content: FileReaderResponse): Promise<any>;\n\n protected startLoading() {\n this.previewService.setLoading(true)\n this.cdr.markForCheck();\n }\n\n protected stopLoading() {\n this.previewService.setLoading(false)\n this.cdr.markForCheck();\n }\n}\n","import {\n Component,\n Directive,\n ElementRef,\n HostListener,\n Input,\n OnDestroy,\n Renderer2,\n ViewContainerRef\n} from \"@angular/core\";\nimport {PreviewService} from \"../services\";\n\n@Directive({ selector: '[tooltip]' ,standalone: true})\nexport class TooltipDirective implements OnDestroy {\n @Input('tooltip') content!: string;\n @Input() delay: number = 500;\n\n private tooltip!: HTMLElement;\n private showTimeout?: any;\n private hideTimeout?: any;\n private positions = ['top', 'bottom', 'left', 'right'];\n private currentPosition = 'top';\n\n constructor(\n private el: ElementRef,\n private renderer: Renderer2,\n private viewContainer: ViewContainerRef,\n private previewService:PreviewService\n ) {}\n\n @HostListener('mouseenter') onMouseEnter() {\n this.clearTimers();\n this.showTimeout = setTimeout(() => this.show(), this.delay);\n }\n\n @HostListener('mouseleave') onMouseLeave() {\n this.clearTimers();\n this.hideTimeout = setTimeout(() => this.hide(), 100);\n }\n\n private show() {\n if(!this.content)return;\n if (!this.tooltip) {\n // 动态创建组件\n const factory = this.viewContainer.createComponent(TooltipComponent);\n this.tooltip = factory.location.nativeElement;\n factory.instance.content = this.content;\n // 立即显示内容\n this.renderer.addClass(this.tooltip, 'visible');\n this.previewService.modalElement?.querySelector('.nfp-modal__overlay').appendChild(this.tooltip);\n factory.changeDetectorRef.detectChanges()\n }\n\n // 计算最佳位置\n const hostRect = this.el.nativeElement.getBoundingClientRect();\n const tooltipRect = this.tooltip.getBoundingClientRect();\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n // 检查每个位置的可用空间\n const spaces: Record<string, number> = {\n top: hostRect.top,\n bottom: viewportHeight - (hostRect.bottom),\n left: hostRect.left,\n right: viewportWidth - (hostRect.right)\n };\n\n // 找到最佳位置\n this.currentPosition = this.positions.reduce((best, current) =>\n spaces[current] > spaces[best] ? current : best\n );\n\n // 根据位置设置样式类\n this.positions.forEach(pos => this.renderer.removeClass(this.tooltip, pos));\n this.renderer.addClass(this.tooltip, this.currentPosition);\n\n // 根据位置计算坐标\n let top, left;\n switch(this.currentPosition) {\n case 'top':\n top = hostRect.top - tooltipRect.height - 8;\n left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;\n break;\n case 'bottom':\n top = hostRect.bottom + 8;\n left = hostRect.left + (hostRect.width - tooltipRect.width) / 2;\n break;\n case 'left':\n top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;\n left = hostRect.left - tooltipRect.width - 8;\n break;\n case 'right':\n top = hostRect.top + (hostRect.height - tooltipRect.height) / 2;\n left = hostRect.right + 8;\n break;\n }\n\n // 确保tooltip不超出视口\n top = Math.max(8, Math.min(viewportHeight - tooltipRect.height - 8, top));\n left = Math.max(8, Math.min(viewportWidth - tooltipRect.width - 8, left));\n\n this.renderer.setStyle(this.tooltip, 'top', `${top}px`);\n this.renderer.setStyle(this.tooltip, 'left', `${left}px`);\n }\n\n private hide() {\n if (this.tooltip) {\n this.renderer.removeClass(this.tooltip, 'visible');\n setTimeout(() => {\n this.viewContainer.clear()\n this.tooltip = null!\n }, 300); // 增加动画时间\n }\n }\n\n private clearTimers() {\n clearTimeout(this.showTimeout);\n clearTimeout(this.hideTimeout);\n }\n\n ngOnDestroy() {\n this.viewContainer.clear();\n }\n}\n@Component({\n selector: 'ngx-file-tooltip',\n template: `{{ content }}`,\n standalone: true,\n styles: [`\n :host {\n position: absolute;\n background: rgba(0, 0, 0, 0.85);\n color: #fff;\n font-size: 14px;\n padding: 6px 8px;\n border-radius: 2px;\n box-shadow: 0 3px 6px -4px rgba(0, 0, 0, 0.12), 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 9px 28px 8px rgba(0, 0, 0, 0.05);\n max-width: 250px;\n min-height: 24px;\n word-wrap: break-word;\n z-index: 999;\n pointer-events: none;\n opacity: 0;\n display: flex;\n justify-content: center;\n align-items: center;\n transform: scale(0.8);\n transform-origin: center;\n transition: opacity 0.2s ease-in-out, transform 0.2s ease-in-out;\n }\n\n :host.visible {\n opacity: 1;\n transform: scale(1);\n }\n\n :host::after {\n content: '';\n position: absolute;\n width: 0;\n height: 0;\n border: 5px solid transparent;\n }\n\n :host.top::after {\n border-top-color: rgba(0, 0, 0, 0.85);\n bottom: -10px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n :host.bottom::after {\n border-bottom-color: rgba(0, 0, 0, 0.85);\n top: -10px;\n left: 50%;\n transform: translateX(-50%);\n }\n\n :host.left::after {\n border-left-color: rgba(0, 0, 0, 0.85);\n right: -10px;\n top: 50%;\n transform: translateY(-50%);\n }\n\n :host.right::after {\n border-right-color: rgba(0, 0, 0, 0.85);\n left: -10px;\n top: 50%;\n transform: translateY(-50%);\n }\n `]\n})\nexport class TooltipComponent {\n @Input() content: string = \"\";\n}\n","import {Component, CUSTOM_ELEMENTS_SCHEMA, HostBinding, Input, NO_ERRORS_SCHEMA} from \"@angular/core\";\nimport {CommonModule} from \"@angular/common\";\nimport {ThemeMode} from \"../../types/theme.types\";\nimport {TooltipDirective} from \"../../directives/tooltip.diretive\";\n\n@Component({\n selector: \"preview-icon\",\n template: `\n <ng-container *ngIf=\"name\">\n <i [tooltip]=\"title\" class=\"fp-font-icon NGX-FILE-PREVIEW\" [class]=\"'nfp-'+name\"\n [style.width]=\"size\" [style.font-size]=\"size\"\n [style.color]=\"color ? color: (themeMode=='light'?'#333333':'#FFFFFF')\"></i>\n </ng-container>\n <ng-container *ngIf=\"svg\">\n <svg class=\"fp-svg-icon\" [style.width]=\"size\" [style.height]=\"size\" [tooltip]=\"title\" aria-hidden=\"true\">\n <use [attr.xlink:href]=\"'#nfp-' + svg\">\"></use>\n </svg>\n </ng-container>\n `,\n styles: [`:host {\n display: inline-block;\n line-height: 0;\n\n .fp-svg-icon {\n width: 1em;\n height: 1em;\n vertical-align: -0.15em;\n fill: currentColor;\n overflow: hidden;\n }\n\n .fp-font-icon {\n color: #FFFFFF;\n display: inline-flex;\n justify-content: center;\n align-items: center;\n aspect-ratio: 1;\n overflow: hidden;\n }\n }\n `],\n imports: [CommonModule, TooltipDirective],\n standalone: true,\n schemas: [CUSTOM_ELEMENTS_SCHEMA, NO_ERRORS_SCHEMA]\n})\nexport class PreviewIconComponent {\n @Input() name: string = \"\";\n @Input() svg: string = \"\";\n @Input({transform: (v: any) => typeof v === 'number' ? `${v}px` : v}) size: number | string = '16px';\n @Input() color?: string;\n @Input() themeMode?: ThemeMode | null;\n @Input() title: string = \"\";\n @Input()\n @HostBinding('style.cursor')\n cursor = 'pointer'\n}\n","import {Optional, Pipe} from \"@angular/core\";\nimport {PreviewService} from \"../services\";\n\n@Pipe({\n name: 'i18n',\n standalone: true\n})\nexport class I18nPipe {\n constructor(private previewService: PreviewService) {\n }\n transform(key: string, ...args:any[]): string {\n const parser = this.previewService.getLangParser();\n return parser.t(key,...args);\n }\n}\n","import {\n AfterViewInit,\n ChangeDetectionStrategy,\n Component,\n ElementRef,\n OnChanges,\n OnDestroy,\n SimpleChanges,\n ViewChild\n} from '@angular/core';\nimport {CommonModule} from '@angular/common';\nimport {BasePreviewComponent} from '../base-preview/base-preview.component';\nimport {PreviewIconComponent} from '../../components/preview-icon/preview-icon.component';\nimport * as XLSX from 'xlsx';\nimport {FileReaderResponse} from \"../../services\";\nimport {I18nPipe} from \"../../i18n\";\n\ninterface TableData {\n headers: string[];\n rows: any[][];\n}\n\n@Component({\n selector: 'ngx-excel-preview',\n standalone: true,\n imports: [CommonModule, PreviewIconComponent, I18nPipe],\n template: `\n <div class=\"excel-container\" #container>\n <div class=\"toolbar\">\n <div class=\"left-controls\">\n <button class=\"tool-btn\" (click)=\"zoomOut()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-out\" [title]=\"'preview.toolbar.zoomOut'|i18n\"></preview-icon>\n </button>\n <span class=\"zoom-text\" (click)=\"resetZoom()\" [title]=\"'preview.toolbar.resetZoom'|i18n\">\n {{ (scale * 100).toFixed(0) }}%\n </span>\n <button class=\"tool-btn\" (click)=\"zoomIn()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-in\" [title]=\"'preview.toolbar.zoomIn'|i18n\"></preview-icon>\n </button>\n </div>\n <div class=\"sheet-controls\" *ngIf=\"sheets.length > 0\">\n <button class=\"sheet-btn\"\n *ngFor=\"let sheet of sheets\"\n [class.active]=\"currentSheet === sheet\"\n (click)=\"switchSheet(sheet)\">\n {{ sheet }}\n </button>\n </div>\n <div class=\"right-controls\">\n <button class=\"tool-btn\" (click)=\"toggleFullscreen()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"fullscreen\" [title]=\"'preview.toolbar.fullscreen'|i18n\"></preview-icon>\n </button>\n </div>\n </div>\n\n <div class=\"preview-container\">\n <div class=\"preview-content\">\n <div class=\"table-wrapper\"\n #tableWrapper\n (mousedown)=\"startDrag($event)\"\n (wheel)=\"handleWheel($event)\"\n [class.dragging]=\"isDragging\"\n [style.transform]=\"'scale(' + scale + ')'\">\n <table *ngIf=\"tableData\">\n <colgroup>\n <col class=\"row-header-col\">\n <col *ngFor=\"let header of tableData.headers\" class=\"data-col\">\n <col *ngFor=\"let i of extraColumns\" class=\"data-col\">\n </colgroup>\n <thead>\n <tr>\n <th class=\"corner-cell\"></th>\n <th *ngFor=\"let header of tableData.headers; let i = index\">\n {{ getColumnName(i) }}\n </th>\n <th *ngFor=\"let i of extraColumns;let j=index\" class=\"empty-column\">\n {{ getColumnName(tableData.headers.length + j) }}\n </th>\n </tr>\n </thead>\n <tbody>\n <tr *ngFor=\"let row of visibleRows; let rowIndex = index\">\n <td class=\"row-header\">{{ getRowNumber(rowIndex) }}</td>\n <td *ngFor=\"let cell of row; let colIndex = index\"\n [class.empty-cell]=\"!cell && cell !== 0\">\n {{ cell }}\n </td>\n <td *ngFor=\"let i of extraColumns\" class=\"empty-cell\"></td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n </div>\n `,\n styleUrls: [\"../../styles/_theme.scss\", \"excel-preview.component.scss\"],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class ExcelPreviewComponent extends BasePreviewComponent implements OnChanges, AfterViewInit, OnDestroy {\n @ViewChild('container') container!: ElementRef<HTMLDivElement>;\n @ViewChild('tableWrapper') tableWrapper!: ElementRef<HTMLDivElement>;\n\n scale = 1;\n sheets: string[] = [];\n currentSheet = '';\n tableData: TableData = {headers: [], rows: []};\n displayRows: any[][] = [];\n extraRows = 100; // 增加额外显示的空行数\n extraColumns = Array(5).fill(0);\n visibleRows: any[][] = [];\n\n private workbook?: XLSX.WorkBook;\n private readonly SCALE_STEP = 0.1;\n private readonly MAX_SCALE = 3;\n private readonly MIN_SCALE = 0.1;\n\n isDragging = false;\n private startX = 0;\n private startY = 0;\n private scrollLeft = 0;\n private scrollTop = 0;\n private mouseMoveListener?: (e: MouseEvent) => void;\n private mouseUpListener?: (e: MouseEvent) => void;\n\n private readonly DEFAULT_SCALE = 1;\n private keydownListener?: (e: KeyboardEvent) => void;\n\n get totalColumns(): number[] {\n const total = (this.tableData.headers.length + this.extraColumns.length) || 0;\n return Array(total).fill(0);\n }\n\n ngOnChanges(changes: SimpleChanges) {\n if (changes['file'] && this.file) {\n this.loadFile();\n }\n }\n\n ngAfterViewInit() {\n this.setupDragListeners();\n this.setupKeyboardListeners();\n }\n\n ngOnDestroy() {\n this.removeDragListeners();\n this.removeKeyboardListeners();\n }\n\n protected override async handleFileContent(content: FileReaderResponse) {\n const {data} = content\n this.workbook = XLSX.read(data, {type: 'array'});\n this.sheets = this.workbook.SheetNames;\n if (this.sheets.length > 0) {\n await this.switchSheet(this.sheets[0]);\n }\n }\n\n private setupDragListeners() {\n this.mouseMoveListener = (e: MouseEvent) => this.onDrag(e);\n this.mouseUpListener = () => this.stopDrag();\n\n document.addEventListener('mousemove', this.mouseMoveListener);\n document.addEventListener('mouseup', this.mouseUpListener);\n }\n\n private removeDragListeners() {\n if (this.mouseMoveListener) {\n document.removeEventListener('mousemove', this.mouseMoveListener);\n }\n if (this.mouseUpListener) {\n document.removeEventListener('mouseup', this.mouseUpListener);\n }\n }\n\n startDrag(e: MouseEvent) {\n // 如果点击的是滚动条,不启动拖动\n const wrapper = this.tableWrapper.nativeElement;\n const rect = wrapper.getBoundingClientRect();\n const isClickOnScrollbarX = e.clientY > (rect.bottom - 12);\n const isClickOnScrollbarY = e.clientX > (rect.right - 12);\n\n if (isClickOnScrollbarX || isClickOnScrollbarY) {\n return;\n }\n\n this.isDragging = true;\n this.startX = e.pageX - wrapper.offsetLeft;\n this.startY = e.pageY - wrapper.offsetTop;\n this.scrollLeft = wrapper.scrollLeft;\n this.scrollTop = wrapper.scrollTop;\n }\n\n private onDrag(e: MouseEvent) {\n if (!this.isDragging) return;\n\n e.preventDefault();\n const wrapper = this.tableWrapper.nativeElement;\n const x = e.pageX - wrapper.offsetLeft;\n const y = e.pageY - wrapper.offsetTop;\n const walkX = (x - this.startX) * 1.5; // 增加一些移动速度\n const walkY = (y - this.startY) * 1.5;\n\n wrapper.scrollLeft = this.scrollLeft - walkX;\n wrapper.scrollTop = this.scrollTop - walkY;\n }\n\n private stopDrag() {\n this.isDragging = false;\n }\n\n async switchSheet(sheetName: string) {\n if (!this.workbook) return;\n\n try {\n const worksheet = this.workbook.Sheets[sheetName];\n const jsonData = XLSX.utils.sheet_to_json<any[]>(worksheet, {header: 1});\n\n // 确保所有行的长度一致\n const maxLength = Math.max(...jsonData.map((row: any[]) => row?.length || 0), 0);\n this.displayRows = jsonData.map((row: any[]) => {\n const paddedRow = Array.isArray(row) ? [...row] : [];\n while (paddedRow.length < maxLength) {\n paddedRow.push(null);\n }\n return paddedRow;\n });\n\n // 添加额外的空行\n const emptyRows = Array(this.extraRows).fill(0).map(() => Array(maxLength).fill(null));\n this.visibleRows = [...this.displayRows, ...emptyRows];\n\n this.tableData = {\n headers: Array(maxLength).fill(''),\n rows: this.displayRows\n };\n\n this.currentSheet = sheetName;\n this.cdr.markForCheck();\n } catch (error) {\n console.error('切换工作表失败:', error);\n }\n }\n\n zoomIn() {\n if (this.scale < this.MAX_SCALE) {\n this.scale = Math.min(this.MAX_SCALE, this.scale + this.SCALE_STEP);\n this.applyZoom();\n }\n }\n\n zoomOut() {\n if (this.scale > this.MIN_SCALE) {\n this.scale = Math.max(this.MIN_SCALE, this.scale - this.SCALE_STEP);\n this.applyZoom();\n }\n }\n\n toggleFullscreen() {\n if (!document.fullscreenElement) {\n document.documentElement.requestFullscreen();\n } else {\n document.exitFullscreen();\n }\n }\n\n getColumnName(index: number): string {\n let name = '';\n let num = index;\n\n do {\n name = String.fromCharCode(65 + (num % 26)) + name;\n num = Math.floor(num / 26) - 1;\n } while (num >= 0);\n\n return name;\n }\n\n getRowNumber(index: number): number {\n return index + 1;\n }\n\n handleWheel(event: WheelEvent) {\n if (event.ctrlKey || event.metaKey) {\n event.preventDefault();\n const delta = event.deltaY || event.detail || 0;\n\n if (delta < 0) {\n this.zoomIn();\n } else {\n this.zoomOut();\n }\n }\n }\n\n private applyZoom() {\n if (this.tableWrapper) {\n const wrapper = this.tableWrapper.nativeElement;\n\n // 保存当前滚动位置的相对百分比\n const scrollLeftPercent = wrapper.scrollLeft / (wrapper.scrollWidth - wrapper.clientWidth);\n const scrollTopPercent = wrapper.scrollTop / (wrapper.scrollHeight - wrapper.clientHeight);\n\n // 应用缩放\n wrapper.style.transform = `scale(${this.scale})`;\n\n // 在下一个事件循环中恢复滚动位置\n setTimeout(() => {\n wrapper.scrollLeft = scrollLeftPercent * (wrapper.scrollWidth - wrapper.clientWidth);\n wrapper.scrollTop = scrollTopPercent * (wrapper.scrollHeight - wrapper.clientHeight);\n });\n }\n this.cdr.markForCheck();\n }\n\n private setupKeyboardListeners() {\n this.keydownListener = (e: KeyboardEvent) => {\n // 按下 Ctrl/Command + 0 重置缩放\n if ((e.ctrlKey || e.metaKey) && e.key === '0') {\n e.preventDefault();\n this.resetZoom();\n }\n };\n\n document.addEventListener('keydown', this.keydownListener);\n }\n\n private removeKeyboardListeners() {\n if (this.keydownListener) {\n document.removeEventListener('keydown', this.keydownListener);\n }\n }\n\n resetZoom() {\n this.scale = this.DEFAULT_SCALE;\n this.applyZoom();\n }\n}\n","import {ChangeDetectionStrategy, Component} from '@angular/core';\nimport {CommonModule} from '@angular/common';\nimport {PreviewIconComponent} from '../../components/preview-icon/preview-icon.component';\nimport {BasePreviewComponent} from \"../base-preview/base-preview.component\";\nimport {FileReaderResponse} from \"../../services\";\nimport {I18nPipe} from \"../../i18n\";\n\n@Component({\n selector: 'ngx-archive-preview',\n standalone: true,\n imports: [CommonModule, PreviewIconComponent, I18nPipe],\n template: `\n <div class=\"archive-container\">\n <div class=\"archive-info\">\n <div class=\"icon\">\n <preview-icon [themeMode]=\"themeMode\" [svg]=\"'zip'\" [size]=\"48\"></preview-icon>\n </div>\n <div class=\"details\">\n <h2>{{ file.name }}</h2>\n <div class=\"meta\">\n <span>{{ 'zip.type'|i18n }}: {{ getArchiveType() }}</span>\n <span>{{ 'zip.size'|i18n }}: {{ formatFileSize(file.size) }}</span>\n </div>\n </div>\n </div>\n </div>\n `,\n styleUrls: [\"../../styles/_theme.scss\", \"archive-preview.component.scss\"],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class ArchivePreviewComponent extends BasePreviewComponent {\n getArchiveType(): string {\n const extension = this.file.name.split('.').pop()?.toLowerCase();\n const that = this;\n const types: { [key: string]: string } = ['zip', 'rar', '7z', 'tar', 'gz'].reduce((ts, key) => Object.assign(ts, {\n [key]: that.t('zip.types.' + key)\n }), {});\n console.log(\"types\", types, this.t(\"zip.types.zip\"))\n return types[extension || ''] || this.t('zip.types.unknown');\n }\n\n protected override async handleFileContent(content: FileReaderResponse) {\n }\n\n formatFileSize(bytes?: number): string {\n if (!bytes) return this.t('zip.unknownSize');\n\n const units = ['B', 'KB', 'MB', 'GB', 'TB'];\n let size = bytes;\n let unitIndex = 0;\n\n while (size >= 1024 && unitIndex < units.length - 1) {\n size /= 1024;\n unitIndex++;\n }\n\n return `${size.toFixed(2)} ${units[unitIndex]}`;\n }\n\n}\n","import {ChangeDetectionStrategy, Component, ElementRef, OnChanges, SimpleChanges, ViewChild} from '@angular/core';\nimport {CommonModule} from '@angular/common';\nimport {BasePreviewComponent} from '../base-preview/base-preview.component';\nimport {PreviewIconComponent} from '../../components/preview-icon/preview-icon.component';\nimport {renderAsync} from 'docx-preview';\nimport {FileReaderResponse} from \"../../services\";\nimport {I18nPipe} from \"../../i18n\";\n\n@Component({\n selector: 'ngx-word-preview',\n standalone: true,\n imports: [CommonModule, PreviewIconComponent, I18nPipe],\n template: `\n <div class=\"word-container\">\n <div class=\"toolbar\">\n <div class=\"left-controls\">\n <button class=\"tool-btn\" (click)=\"zoomOut()\" [disabled]=\"scale <= MIN_SCALE\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-out\" [title]=\"'preview.toolbar.zoomOut'|i18n\"></preview-icon>\n </button>\n <span class=\"zoom-text\" (click)=\"resetZoom()\" [title]=\"'preview.toolbar.resetZoom'|i18n\">\n {{ (scale * 100).toFixed(0) }}%\n </span>\n <button class=\"tool-btn\" (click)=\"zoomIn()\" [disabled]=\"scale >= MAX_SCALE\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-in\" [title]=\"'preview.toolbar.zoomIn'|i18n\"></preview-icon>\n </button>\n </div>\n <div class=\"right-controls\">\n <button class=\"tool-btn\" (click)=\"toggleFullscreen()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"fullscreen\" [title]=\"'preview.toolbar.fullscreen'|i18n\"></preview-icon>\n </button>\n </div>\n </div>\n\n <div class=\"preview-container\"\n #previewContainer\n (wheel)=\"handleWheel($event)\"\n (pointerdown)=\"startDrag($event)\"\n (pointermove)=\"onDrag($event)\"\n (pointerup)=\"stopDrag($event)\"\n [class.dragging]=\"isDragging\">\n <div class=\"content-wrapper\">\n <div #content\n class=\"preview-content\"\n [style.transform]=\"'scale(' + scale + ')'\">\n </div>\n </div>\n </div>\n </div>\n `,\n styleUrls: [\"../../styles/_theme.scss\", \"word-preview.component.scss\"],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class WordPreviewComponent extends BasePreviewComponent implements OnChanges {\n @ViewChild('content') content!: ElementRef<HTMLDivElement>;\n @ViewChild('previewContainer') previewContainer!: ElementRef<HTMLDivElement>;\n\n // 缩放相关常量\n protected readonly MIN_SCALE = 0.25;\n protected readonly MAX_SCALE = 4;\n protected readonly SCALE_STEP = 0.1;\n protected readonly DEFAULT_SCALE = 1;\n\n // 状态\n protected scale = 1;\n protected isDragging = false;\n\n // 拖拽相关\n private startX = 0;\n private startY = 0;\n private scrollLeft = 0;\n private scrollTop = 0;\n\n ngOnChanges(changes: SimpleChanges) {\n if (changes['file']) {\n this.loadFile()\n }\n }\n override async handleFileContent(content: FileReaderResponse) {\n try {\n await renderAsync(content.data, this.content.nativeElement, undefined, {\n className: 'docx-content',\n inWrapper: false,\n ignoreWidth: false,\n ignoreHeight: false,\n ignoreFonts: false,\n breakPages: true,\n useBase64URL: true,\n });\n } catch (error) {\n console.log(\"error\", error)\n }\n this.cdr.markForCheck();\n }\n\n handleWheel(event: WheelEvent) {\n if (event.ctrlKey || event.metaKey) {\n event.preventDefault();\n const delta = event.deltaY < 0 ? 1 : -1;\n if (delta > 0) {\n this.zoomIn();\n } else {\n this.zoomOut();\n }\n }\n }\n\n startDrag(event: PointerEvent) {\n if (event.button !== 0) return;\n\n this.isDragging = true;\n const container = this.previewContainer.nativeElement;\n this.startX = event.clientX;\n this.startY = event.clientY;\n this.scrollLeft = container.scrollLeft;\n this.scrollTop = container.scrollTop;\n\n container.setPointerCapture(event.pointerId);\n event.preventDefault();\n }\n\n onDrag(event: PointerEvent) {\n if (!this.isDragging) return;\n\n const container = this.previewContainer.nativeElement;\n const deltaX = event.clientX - this.startX;\n const deltaY = event.clientY - this.startY;\n\n container.scrollLeft = this.scrollLeft - deltaX;\n container.scrollTop = this.scrollTop - deltaY;\n }\n\n stopDrag(event: PointerEvent) {\n if (!this.isDragging) return;\n\n this.isDragging = false;\n this.previewContainer.nativeElement.releasePointerCapture(event.pointerId);\n }\n\n\n zoomIn() {\n if (this.scale < this.MAX_SCALE) {\n this.scale = Math.min(this.MAX_SCALE, this.scale + this.SCALE_STEP);\n this.applyZoom();\n }\n }\n\n zoomOut() {\n if (this.scale > this.MIN_SCALE) {\n this.scale = Math.max(this.MIN_SCALE, this.scale - this.SCALE_STEP);\n this.applyZoom();\n }\n }\n\n resetZoom() {\n this.scale = this.DEFAULT_SCALE;\n this.applyZoom();\n }\n\n private applyZoom() {\n if (this.content?.nativeElement && this.previewContainer?.nativeElement) {\n const container = this.previewContainer.nativeElement;\n const rect = container.getBoundingClientRect();\n\n // 获取当前滚动中心点\n const centerX = (container.scrollLeft + rect.width / 2) / this.scale;\n const centerY = (container.scrollTop + rect.height / 2) / this.scale;\n\n // 更新缩放\n this.content.nativeElement.style.transform = `scale(${this.scale})`;\n this.content.nativeElement.style.transformOrigin = 'top left';\n\n // 调整滚动位置,确保视图保持缩放中心一致\n requestAnimationFrame(() => {\n container.scrollLeft = centerX * this.scale - rect.width / 2;\n container.scrollTop = centerY * this.scale - rect.height / 2;\n });\n }\n this.cdr.markForCheck();\n }\n\n toggleFullscreen() {\n if (!document.fullscreenElement) {\n document.documentElement.requestFullscreen();\n } else {\n document.exitFullscreen();\n }\n }\n}\n\n","import {ChangeDetectionStrategy, Component, ElementRef, OnInit, ViewChild} from '@angular/core';\nimport {CommonModule} from '@angular/common';\nimport {NgxExtendedPdfViewerComponent, NgxExtendedPdfViewerModule} from 'ngx-extended-pdf-viewer';\nimport {PreviewIconComponent} from '../../components/preview-icon/preview-icon.component';\nimport {BasePreviewComponent} from \"../base-preview/base-preview.component\";\nimport {FileReaderResponse} from \"../../services\";\nimport {I18nPipe} from \"../../i18n\";\n\n@Component({\n selector: 'ngx-pdf-preview',\n standalone: true,\n imports: [CommonModule, NgxExtendedPdfViewerModule, PreviewIconComponent, I18nPipe],\n template: `\n <div class=\"pdf-container\">\n <!-- 工具栏 -->\n <div class=\"toolbar\">\n <div class=\"left-controls\">\n <div class=\"control\" (click)=\"zoomOut()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-out\" [title]=\"'preview.toolbar.zoomOut'|i18n\"></preview-icon>\n </div>\n <span (click)=\"resetZoom()\">{{ zoom == \"page-fit\" ? \"100%\" : zoom }}%</span>\n <div class=\"control\" (click)=\"zoomIn()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-in\" [title]=\"'preview.toolbar.zoomIn'|i18n\"></preview-icon>\n </div>\n <div class=\"control\" (click)=\"autoFit()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"auto-fit\" [title]=\"'preview.toolbar.autoFit'|i18n\"></preview-icon>\n </div>\n </div>\n\n <div class=\"right-controls\">\n <div class=\"control\" (click)=\"rotate(-90)\">\n <preview-icon [themeMode]=\"themeMode\" name=\"rotate-90\" [title]=\"'preview.toolbar.rotate-90'|i18n\"></preview-icon>\n </div>\n <div class=\"control\" (click)=\"rotate(90)\">\n <preview-icon [themeMode]=\"themeMode\" name=\"rotate90\" [title]=\"'preview.toolbar.rotate90'|i18n\"></preview-icon>\n </div>\n <div class=\"control\" (click)=\"reset()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"reset\" [title]=\"'preview.toolbar.reset'|i18n\"></preview-icon>\n </div>\n </div>\n </div>\n <!-- PDF查看器 -->\n <div class=\"viewer-container\" #viewerContainer>\n <ngx-extended-pdf-viewer\n [class.hidden]=\"isLoading|async\"\n [src]=\"file.url\"\n [rotation]=\"rotation\"\n [zoom]=\"zoom\"\n (currentZoomFactor)=\"onScaleChange($event)\"\n [(page)]=\"currentPage\"\n [backgroundColor]=\"'rgba(0,0,0,0)'\"\n [showSidebarButton]=\"false\"\n [textLayer]=\"true\"\n [showToolbar]=\"false\"\n [showTextEditor]=\"false\"\n [showHandToolButton]=\"false\"\n [showFindButton]=\"false\"\n [showPagingButtons]=\"false\"\n [showZoomButtons]=\"false\"\n [showPresentationModeButton]=\"false\"\n [showOpenFileButton]=\"false\"\n [showPrintButton]=\"false\"\n [showDownloadButton]=\"false\"\n [showSecondaryToolbarButton]=\"false\"\n [showRotateButton]=\"false\"\n [showSpreadButton]=\"false\"\n [showPropertiesButton]=\"false\"\n (pagesLoaded)=\"pdfLoaded()\"\n style=\"width: 100%; height: 100%;\"\n ></ngx-extended-pdf-viewer>\n </div>\n </div>\n `,\n styleUrls: [\"../../styles/_theme.scss\", \"./pdf-preview.component.scss\"],\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class PdfPreviewComponent extends BasePreviewComponent implements OnInit{\n zoom: any = \"page-fit\";// 'auto'|'page-actual'|'page-fit'|'page-width'|\n rotation: 0 | 90 | 180 | 270 = 0;\n currentPage = 0;\n @ViewChild(NgxExtendedPdfViewerComponent) pdfViewer!: NgxExtendedPdfViewerComponent;\n @ViewChild('viewerContainer') viewerContainer?: ElementRef<HTMLDivElement>\n ngOnInit(){\n this.startLoading()\n }\n protected override async handleFileContent(content: FileReaderResponse) {\n }\n\n pdfLoaded() {\n this.stopLoading()\n this.autoFit()\n }\n\n zoomIn() {\n this.zoom = Math.floor(Math.min(this.zoom * 1.2, 300));\n }\n\n zoomOut() {\n this.zoom = Math.floor(Math.max(this.zoom / 1.2, 10));\n }\n\n autoFit() {\n this.zoom = 'page-fit';\n }\n\n resetZoom() {\n this.zoom = 100;\n }\n\n reset() {\n this.resetZoom();\n this.rotation = 0;\n }\n\n rotate(degrees: number) {\n this.rotation = (this.rotation + degrees + 360) % 360 as 0 | 90 | 180 | 270;\n }\n\n onScaleChange($event:number) {\n const zoomNum = Math.floor(Number($event) * 100);\n if (Number.isNaN(zoomNum) || this.zoom == zoomNum) {\n return\n }\n this.zoom = zoomNum\n this.cdr.markForCheck()\n }\n\n}\n","import {AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, ViewChild} from '@angular/core';\nimport {CommonModule} from '@angular/common';\nimport {BasePreviewComponent} from '../base-preview/base-preview.component';\nimport {PreviewIconComponent} from \"../../components/preview-icon/preview-icon.component\";\nimport {FileReaderResponse} from \"../../services\";\nimport {I18nPipe} from \"../../i18n/i18n.pipe\";\n\n@Component({\n selector: 'ngx-image-preview',\n standalone: true,\n imports: [CommonModule, PreviewIconComponent, I18nPipe],\n template: `\n <div class=\"image-preview\"\n (mousedown)=\"startDrag($event)\"\n (mousemove)=\"onDrag($event)\"\n (mouseup)=\"stopDrag()\"\n (mouseleave)=\"stopDrag()\"\n (wheel)=\"handleWheel($event)\">\n <div class=\"image-wrapper\"\n #imageWrapper\n [style]=\"transformStyle\"\n [class.is-moving]=\"isDragging\">\n <img #previewImage\n [src]=\"file.url\"\n [style.display]=\"(isLoading|async) ? 'none' : 'block'\"\n (load)=\"onImageLoad()\"\n alt=\"preview\"/>\n </div>\n\n <div class=\"image-info\" *ngIf=\"!(isLoading|async)\">\n <span class=\"filename\">{{ file.name }}</span>\n <span class=\"dimensions\">{{ imageWidth }} × {{ imageHeight }}</span>\n </div>\n\n <div class=\"toolbar\" *ngIf=\"!(isLoading|async)\">\n <div class=\"tool-group\">\n <div class=\"control\" (click)=\"zoomOut()\" [class.disabled]=\"zoom <= minZoom\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-out\" [title]=\"'preview.toolbar.zoomOut'|i18n\"></preview-icon>\n </div>\n <span class=\"zoom-text\">{{ (zoom * 100).toFixed(0) }}%</span>\n <div class=\"control\" (click)=\"zoomIn()\" [class.disabled]=\"zoom >= maxZoom\">\n <preview-icon [themeMode]=\"themeMode\" name=\"zoom-in\" [title]=\"'preview.toolbar.zoomIn'|i18n\"></preview-icon>\n </div>\n </div>\n\n <div class=\"divider\"></div>\n\n <div class=\"tool-group\">\n <div class=\"control\" (click)=\"rotate(-90)\">\n <preview-icon [themeMode]=\"themeMode\" name=\"rotate-90\" [title]=\"'preview.toolbar.rotate-90'|i18n\"></preview-icon>\n </div>\n <div class=\"control\" (click)=\"rotate(90)\">\n <preview-icon [themeMode]=\"themeMode\" name=\"rotate90\" [title]=\"'preview.toolbar.rotate90'|i18n\"></preview-icon>\n </div>\n </div>\n\n <div class=\"divider\"></div>\n\n <div class=\"tool-group\">\n <div class=\"control\" (click)=\"autoFit()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"auto-fit\" [title]=\"'preview.toolbar.autoFit'|i18n\"></preview-icon>\n </div>\n <div class=\"control\" (click)=\"originSize()\">\n <preview-icon [themeMode]=\"themeMode\" name=\"origin-size\" [title]=\"'previ