Skip to content
Yusuf Özdemir
ALL ARTICLES

VueFinder 4.5: preview modal'ı büyüdü

11 MIN READ 2,120 WORDS
ALSO IN English

VueFinder 4.5, 4.4'ün kaldığı yerden devam ediyor — 4.4'te editor ve preview altyapısı ön plandaydı, 4.5 bu hattı katlıyor. Preview modal'ı baştan UX'lendi, image preview gerçek bir multi-tool editor'a kavuştu ve bir süredir biriken birkaç pürüz temizlendi.

4.4 yazısındaki gibi "neden değişti" perspektifiyle.

Preview modal yeniden yazıldı

image

Eski preview modal'ında iki kontrol yüzeyi birbiriyle çekişiyordu: [‹] [Kapat] [›] [İndir] taşıyan global footer ve Text / Csv / Image içindeki [Kaydet] [İptal] veya [Kes] [İptal] taşıyan previewer-içi header. İki ayrı yer, iki ayrı görsel dil; mobilde footer, baş parmağın rahat erişemediği aksiyonlar için ~80px dikey alan çalıyordu.

4.5 ikisini tek tutarlı bir kabuğa indiriyor.

Yeni chrome layout

Üst bar (PreviewChrome.vue) sadece şunları tutuyor: info popover · dosya adı · sayfalama · download · close. Edit butonu burada yok. Dosya adı truncate olur, aksiyonlar hiç sıkışmaz. i ikonu border'sız ve dosya adının solunda — başlık kümesinin parçası gibi okunuyor, başka bir aksiyon butonu gibi değil. Download tek ikonlu bir toggle: tıklayınca açılan popover gerçek indirme linkini + "sağ-tık → Save link as" notunu içeriyor (mobilde gizli çünkü sağ-tık diye bir şey yok).

Modal'ın mevcut footer'ı edit aksiyonlarını sahipleniyor: view modunda [Edit], edit modunda [Cancel] [Save]. Bir flex container'a sarıldığı için mode değişiminde yükseklik aynı kalıyor — küçük bir detay ama Edit'e basınca modal'ın 40px sıçraması tam da o "micro-frustration" türü, biriktikçe sinir bozucu.

PreviewControls contract'ı

"Chrome yukarıda, edit lifecycle aşağıda" koordinasyonu için ortak bir protokol gerekti. Yeni dosya types/preview.ts:

interface PreviewControls {
  isEditable: ComputedRef<boolean>
  isEditing: ComputedRef<boolean>
  isDirty: ComputedRef<boolean>
  primaryActionLabel: ComputedRef<string | null>  // 'Save', 'Crop', ...
  enterEdit: () => void | Promise<void>
  commitEdit: () => void | Promise<void>
  cancelEdit: () => void | Promise<void>
  extraInfo?: ComputedRef<{ label: string; value: string }[]>  // [i] popover için
}

Her previewer setup'ta usePreviewControls(...)'u kendi reactive contract'ı ile çağırıyor; chrome app.modal üzerindeki tek bir shallowRef'i okuyor (en son mount olan previewer kendini oraya register ediyor). PDF/Video/Audio/Default hiç register etmiyor — controls === null olunca contract okumaları otomatik olarak isEditable=false'a düşüyor, Edit butonu kendiliğinden saklanıyor.

Image, extraInfo ile bitmap yüklendiğinde intrinsic genişlik × yükseklik'i [i] popover'ına ekliyor — chrome'un image boyutu hakkında hiçbir şey bilmesine gerek kalmıyor.

Birkaç saatimi yiyen reactive() tuzağı

ServiceContainer.ts, app instance'ını reactive(...) ile sarıyor. Bu da reactive proxy üzerinden erişilen ref'leri otomatik unwrap ediyor. Chrome'un ilk versiyonu app.modal.controls.value okuyordu — tek başına test edilince çalışan, ama reactive layer ref'i unwrap ettiği için undefined.value (TypeError) döndüren bir kod. Düzeltme tek karakterlik silme (app.modal.controls yeter), ama semptom — "registration log başarılı diyor, Edit hâlâ görünmüyor" — aşırı yardımsızdı. Notuma şöyle düştüm: "evet, reactive proxy'lerde ref'ler unwrap olur; hayır, docs bunu bağırarak söylemiyor."

Edit + dirty state'i gözden kaçırılamaz hale getirme

Önce edit modu sadece action bar'ın Edit → Save+Cancel'a dönmesiyle, dirty ise dosya adının önündeki tek bir ile sinyal veriyordu. Dikkat içeriğe odaklıyken kaçırması kolaydı.

4.5, alttaki sayfalama chip'ini edit modunda amber bir state chip'iyle değiştiriyor:

  • View mode → 3 / 27 (sade gri sayfalama)
  • Edit + clean → EDITING (outline amber)
  • Edit + dirty → UNSAVED (solid amber, beyaz text)

Aynı slot, aynı box-model (ikisi de rounded-full + 1px border, identik padding ve line-height) → chip swap'inde modal yerinden oynamıyor. Bonus olarak modal'ın üst kenarında 3px amber inset stripe — uzun bir text dosyasına bakıyor olsan bile çevresel görüşün "edit modundasın" diyor.

Tek close path

Eskiden modal'ı kapatan üç ayrı yol vardı: [Close] butonu, Esc, overlay-click. Hepsi ayrı wire'lıydı. Yeni requestClose() akışı tek guard'dan geçiyor: isEditing && isDirty ise native window.confirm("Discard unsaved changes?") fırlatılıyor, sonra gerçekten kapanıyor. ModalLayout bir onRequestClose prop'u expose ediyor; istediğin modal bu pattern'e opt-in olabilir. ModalPreview ilk kullanıcı.

Mobil: card-swap swipe gesture'ı

Side chevron'lar desktop'ta harika ama mobilde hover olmadan görünmez. 4.5 gerçek drag-with-animation swipe ekliyor:

  • Chrome title'a veya alt status strip'e dokun
  • Yatay drag → modal body parmakla beraber translate olur
  • ~22% viewport width'i geçince → "stage 1" body'yi tamamen ekran dışına animate eder (220ms), sonra item swap olur, body karşı tarafa pre-position'lanır ve merkeze animate olarak girer
  • Threshold altıysa → snap-back

Card-swap koreografisi (içerik sabit modal içinde kayması değil) "yandaki dosya gerçekten yan komşu" hissini veren şey. iOS Photos / Instagram Stories patterni.

Gesture allowlist ile scoped, blocklist değil. Sadece title'da veya status strip'te başlayan touch'lar swipe'ı arm ediyor. Başka yerde — image pan, PDF scroll, video controls, text selection, cropper handles — her şey alttaki bileşene ait, modal navigation tetiklenmiyor. Yeni previewer eklemek interactive-element selector'ını uzatmak demek değil; default'tan güvenli.

CodeMirror readonly görsel polish

Editor view modunda açıkken (edit değilken) bire bir editor gibi görünüyordu — aynı beyaz bg, yanıp sönen caret, aktif satır vurgusu. Kullanıcılar tıklayıp yazmaya çalışıyor, hiçbir şey olmuyordu. 4.5 readonly'yi belirgin şekilde ayırıyor: gri yüzey (bg-secondary), mute text, caret yok, aktif satır highlight yok, gutter daha da tonlu. Edit'e geçince tam kontrastlı editor görünümü geri geliyor → "şimdi yazabilirim" net.

CodeMirror word-wrap toggle

Editor'ın sağ-üst köşesinde küçük bir ikon; EditorView.lineWrapping'i Compartment ile flip ediyor. localStorage['vuefinder:codemirror-wrap']'a persist olduğu için tercih seanslar arası kalıyor. Hem readonly hem edit modunda çalışıyor.

Image editor: multi-tool

image

4.4'teki image preview'ın edit modu seni doğrudan bir vue-advanced-cropper'a ve tek bir "Crop" butonuna düşürüyordu — ya dosyayı üzerine yaz ya iptal et. Rotate yok, filter yok, başka bir şey yok. 4.5 bunu tab'lı bir editor'a dönüştürüyor (iOS Photos tarzı üst şerit):

  • Cropvue-advanced-cropper + aspect-ratio chip'leri (Original / 1:1 / 4:3 / 16:9 / 9:16)
  • Rotate — ⟲ / ⟳ 90° + Flip H/V
  • Grayscale — toggle + CSS live preview
  • Adjust — Brightness / Contrast / Saturation slider'ları (−100..+100)

Model: destructive working canvas

Operation-stack yaklaşımını düşündüm (originalImage + operations[] tut, preview'ı stack'ten replay ederek render et — non-destructive, undoable). v1 için reddettim: çok daha fazla kod, ve kullanıcının istediği "Apply" zihinsel modeli destructive bake'lerle doğal eşleşiyor. Image.vue üzerinde tek bir workingDataUrl ref'i, her tab'ın Apply'ı onu mutate ediyor, alt Save upload'lıyor. Cancel komple working URL'i orijinale geri çeviriyor. Session içi undo yok — Cancel-and-restart bu büyüklükte yeterli.

Bu modeli seçmek editor kodunu ~750 satır net'te (component + canvas helper'ları + CSS) tuttu, ~2000+'a çıkmadı. "Undo" feature'ı roadmap'te bekliyor; isteyen olursa eklenir.

Live preview, Apply'da bake

Tüm filter-tarzı operasyonlar düz CSS ile preview edilir:

// Live preview
<img :src="workingUrl" :style="{ filter: 'brightness(1.2) contrast(0.9)' }" />

// Apply'da — working canvas'a bake et
ctx.filter = 'brightness(1.2) contrast(0.9)'
ctx.drawImage(img, 0, 0)
return canvas.toDataURL(mime, 0.92)

İki yerde de aynı filter fonksiyonu string'i kullanılıyor → live preview, Apply'ın üreteceği şeye byte-accurate. ctx.filter modern tarayıcıların hepsinde desteklendiği için manuel getImageData → piksel loop → putImageData yazmaya gerek yok.

Önem taşıyan iki küçük detay

Rotate yönü aynı kalıyor. Sağa rotate'i dört kere tıkla: 0° → 90° → 180° → 270° → 0°. Ama CSS en kısa yoldan animate ettiği için 4. tıklama görsel olarak 270° geriye dönüyor. Fix: pendingRotation'ı unbounded derece olarak tut (0 → 90 → 180 → 270 → 360 → 450 → ...). Artık CSS her seferinde +90° aynı yönde animate ediyor. 0/90/180/270'e modulo sadece bake zamanında — canvas concrete quarter-turn istiyor.

Crop "dirty" gerçekten sürüklemeden tetiklenmiyor. vue-advanced-cropper init sırasında birkaç kez @change fırlatıyor (auto-zoom, image-fit). Guard olmadan, Crop'u açıp hemen Cancel'a basmak unsaved-changes uyarısı çıkarıyordu. enterEdit'ten sonraki 400ms'lik settle penceresi @change'leri yutuyor; cropper-touched flag'i sadece pencere kapandıktan sonra true'ya dönüyor.

Go to Folder modal

Önce prompt(t('Enter folder path:')). Tema'lı modal ile değiştirildi:

  • Live folder autocomplete sen yazarken, parent segment'in adapter.list()'i ile. 150ms debounce; monotonic lookup id stale response'ları düşürüyor — sıra dışı gelen sonuçlar tazelerini ezemiyor.
  • Storage prefix mode henüz :// yazılmadıysa — öneriler storage adları.
  • Recent paths (son 4) input boşken görünür, localStorage['vuefinder:recent-paths']'te saklanır. Her recent satırının (input'a yaz, düzenle) ve × (sil) butonu var.
  • Klavye: ↑/↓ navigate, Tab folder'ı input'a tamamlar, Enter navigate, Esc kapat.
  • Inline error'laralert() yerine.

Uygulamanın geri kalanının kullandığı aynı TanStack-Query cache'i kullanıyor, yani local://docs/'u bir kez listelersen ikinci ziyaret refetch etmiyor.

Bahsedilmeye değer fix'ler

Hard refresh sonrası bazen kırık thumbnail'ler

Hard refresh'te tarayıcı tüm thumbnail isteklerini aynı anda fırlatıyor. Yük altında bazıları başarısız oluyor — vanilla-lazyload da başarısız elementleri errored olarak işaretliyor, otomatik retry yok. Tarayıcı sonra broken-image ikonunu render ediyor → kullanıcıların raporladığı "?" thumbnail'i.

Fix useLazyLoad.ts'de: restore_on_error: true placeholder'ı geri koyuyor (broken icon yerine), artı custom callback_error her başarısız thumbnail'i jittered exponential backoff ile 4 kez yeniden deniyor (≈0.6s, 1.2s, 2.4s, 4.8s). Tükenince placeholder kalıyor. Element başına retry sayısını WeakMap tutuyor; detach olan node'lar GC'leniyor.

Breadcrumb'taki refresh butonu loading sırasında Close (×) ikonuna dönüp vf-fetch-abort emit ediyor — ama dinleyen yoktu. AdapterManager üzerinde yeni cancelOpen() queryClient.cancelQueries({ queryKey: ['adapter', 'list', ...] }) çağırıyor; bu da TanStack Query'nin query function'a verdiği AbortSignal'i tetikliyor, driver da onu fetch()'e geçiriyor. Network-katmanı iptal, sadece UI flip değil. open() resulting CancelledError'u yakalıyor → context menu navigation'ları gibi caller'larda unhandled rejection olmuyor.

enabled() predicate'i olmayan menübar dropdown item'ları sessizce hiçbir şey yapmıyordu

Bu utanç verici olanı. MenuBar.vue'daki click handler şöyleydi:

@click.stop="item.enabled && item.enabled() ? handleMenuAction(item.action) : null"

item.enabled undefined ise (ki Upload, New Folder, New File, Search, Preview, Copy Download URL, Rename, Delete, Refresh, Persist Path, Metric Units ve tüm storage switch'leri için öyleydi) short-circuit null döndürüyor, action hiç fire etmiyordu. Fix tek koşul flipi: eksik enabled "her zaman enabled" sayılır. 12+ menü aksiyonu uyandı.

Backend: generic "Filesystem error occurred" → gerçek mesaj + doğru status

Companion Laravel app vuefinder-api-php her Flysystem FilesystemException'ı yutup düz 500 + "Filesystem error occurred." dönüyordu. Yeni Go to Folder modal'ına saçma sapan path yazınca bu generic blob'la karşılaşıyordun.

VueFinderExceptionHandler.php güncellendi: (a) "does not exist" / "no such file" / "not found" pattern'lerini tespit edip temiz 404 path_not_found döndürüyor, (b) geri kalan her şey için underlying exception mesajını geçiriyor. Frontend artık generic 500 yerine kullanışlı hata gösteriyor.

İç tarafta

vue-tsc emit, --noEmit'in kaçırdığını yakaladı

npm run build'in type generation adımı tam emit ile vue-tsc koşuyor. Bu dev-loop'taki --noEmit kontrolünün geçirdiği birkaç number | null ihlalini ve Array.map<T>()'teki bir inference quirk'ünü yakaladı. .d.ts ship ediyorsan bilmekte fayda var: --noEmit ≠ build-mode strictness.

i18n: 32 yeni key

Bu release ~32 yeni string ekledi. en + tr tam çevrili; diğer 16 locale (ar, de, es, fa, fr, he, hi, it, ja, nl, pl, pt, ru, sv, zhCN, zhTW) sync-locales.mjs adlı küçük bir script ile English fallback placeholder'lar aldı — script en.js'e karşı diff alıp eksik key'leri her locale'in kapanış brace'inden önce inject ediyor. Translation PR'ları beklenir.

Sırada ne var

Roadmap'te bekleyenler:

  • Image editor: Annotate tab — pencil + eraser + text + line / arrow / rectangle / circle, color picker, undo. Native canvas + overlay layer. Tasarlandı ama release'i ship edilebilir tutmak için ertelendi.
  • Image editor: Filter presets — mevcut bakeFilter pipeline'ı üstünde Instagram-style preset chip'leri (Sepia / Vintage / Mono / Cool / Warm). Hızlı ekleme.
  • Image editor: Resize / Compress tab — width/height + JPEG quality slider'ı + live byte-size tahmini.
  • Custom previewer plugin APIPreviewControls contract'ı bunun yarısı; bir registry/API third-party previewer kaydı için döngüyü kapatır (.dwg için three.js, zip-içeriği viewer'ı vs. plug edilebilir hale gelir).

npm install vuefinder@4.5.0 release'i çekiyor. Issue, çeviri ve feedback: github.com/n1crack/vuefinder. 4.6'da görüşmek üzere.

More to read