Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ Sağ üstteki **Export** düğmesiyle mevcut sekme içeriği şu biçimlerde dı
### Dil Desteği
Sağ üstteki **EN / TR** geçişiyle arayüz dili değiştirilebilir. Tercih `localStorage`'a kaydedilir.

### Örnekleri yeniden yükleme
Üst bardaki **Örnekler** düğmesi güncel dildeki örnek içeriği yeni bir notebook olarak açar. Mevcut notebook'ları veya `localStorage` verisini silmez; böylece daha önce kullanıcı verisi kaydedilmiş olsa bile örnekler tekrar yüklenebilir.

### Offline durum, yedekleme ve kayıt
- Alt durum çubuğu tarayıcının çevrimiçi / çevrimdışı durumunu anlık gösterir.
- **Yedek** düğmesi tüm notebook'ları, aktif sekmeyi ve temel görünüm tercihlerini tek bir sürümlü JSON dosyasında dışa aktarır.
Expand Down Expand Up @@ -153,6 +156,7 @@ GitHub Pages sürümü installable PWA olarak yayınlanır. Manifest, uygulama i

- `start_url` ve `scope` relative (`./`) tutulduğu için proje `/mathnote/` gibi GitHub Pages alt yollarında çalışır.
- İlk başarılı ziyarette uygulama shell'i, JavaScript/CSS, KaTeX fontları, manifest ve ikonlar precache edilir.
- `app.js`, `app.css` ve manifest gibi değişebilir çekirdek dosyalar ağ varken **network-first**, çevrimdışıyken cache fallback ile yüklenir. Bu, yeni deploy sonrasında eski JavaScript'in yanlışlıkla servis edilmesini önler.
- Yeni build'de dosya içeriğinden yeni bir cache sürümü üretilir; service worker aktive olduğunda eski MathNote cache'leri temizlenir.
- Service worker yalnızca HTTP/HTTPS üzerinde kaydolur. Bu nedenle `dist/index.html` dosyasını doğrudan `file://` ile açma desteği korunur.
- Destekleyen tarayıcılar MathNote'u masaüstüne veya ana ekrana bağımsız uygulama olarak kurabilir.
Expand Down
5 changes: 3 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
border-radius: 6px;
padding: 2px;
}
#export-btn, #backup-btn, #info-btn {
#export-btn, #backup-btn, #examples-btn, #info-btn {
padding: 4px 10px;
border: 1px solid #ced4da;
background: #fff;
Expand All @@ -82,7 +82,7 @@
gap: 4px;
white-space: nowrap;
}
#export-btn:hover, #backup-btn:hover, #info-btn:hover {
#export-btn:hover, #backup-btn:hover, #examples-btn:hover, #info-btn:hover {
background: #f1f3f5;
border-color: #1A71C2;
color: #1A71C2;
Expand Down Expand Up @@ -968,6 +968,7 @@
<span id="app-logo">mathnote</span>
<div id="topbar-right">
<button id="info-btn" title="Information / Bilgi">Info</button>
<button id="examples-btn" title="Load examples">Examples</button>
<button id="backup-btn" title="Backup / Restore">Backup</button>
<button id="export-btn">&#8593; Export</button>
<div id="lang-switcher">
Expand Down
15 changes: 15 additions & 0 deletions scripts/verify-offline.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,19 @@ if (!html.includes('rel="manifest" href="./manifest.webmanifest"')) {
violations.push('index.html: manifest link missing or not relative');
}

if (!html.includes('id="examples-btn"')) {
violations.push('index.html: examples button missing');
}

const app = await readFile(new URL('app.js', root), 'utf8');
if (!app.includes("register('./sw.js'") && !app.includes('register("./sw.js"')) {
violations.push('app.js: relative service worker registration missing');
}

if (!app.includes('examples-btn') || !app.includes('getDefaultContent')) {
violations.push('app.js: examples loader wiring missing');
}

const manifest = JSON.parse(await readFile(new URL('manifest.webmanifest', root), 'utf8'));
if (manifest.start_url !== './') violations.push('manifest: start_url must be ./');
if (manifest.scope !== './') violations.push('manifest: scope must be ./');
Expand All @@ -81,6 +89,13 @@ if (sw.includes('__CACHE_NAME__') || sw.includes('__PRECACHE_ASSETS__')) {
if (!/mathnote-[a-f0-9]{12}/.test(sw)) {
violations.push('sw.js: content-derived cache version missing');
}

if (!sw.includes('NETWORK_FIRST_PATHS') || !sw.includes('networkFirst(request')) {
violations.push('sw.js: mutable core assets are not network-first');
}
if (sw.includes('caches.match(')) {
violations.push('sw.js: cross-cache lookup can serve stale assets');
}
for (const asset of ['./index.html', './app.js', './app.css', './manifest.webmanifest', './icons/icon-192.png', './icons/icon-512.png']) {
if (!sw.includes(asset)) violations.push(`sw.js: precache entry missing for ${asset}`);
}
Expand Down
17 changes: 17 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ const translations = {
en: {
// Top bar
newFileBtn: 'New file',
examplesBtn: 'Examples',
examplesTitle: 'Load examples into a new notebook',
examplesFileName: 'examples.math',

// Modal
modalTitle: 'Create New File',
Expand Down Expand Up @@ -208,6 +211,9 @@ const translations = {
tr: {
// Top bar
newFileBtn: 'Yeni dosya',
examplesBtn: 'Örnekler',
examplesTitle: 'Örnekleri yeni bir not defterine yükle',
examplesFileName: 'ornekler.math',

// Modal
modalTitle: 'Yeni Dosya Oluştur',
Expand Down Expand Up @@ -418,6 +424,8 @@ function setLanguage(lang) {
function updateUILanguage() {
// Update tooltips
document.getElementById('new-tab-btn').title = t('newFileBtn');
document.getElementById('examples-btn').textContent = t('examplesBtn');
document.getElementById('examples-btn').title = t('examplesTitle');
document.getElementById('zoom-in').title = t('zoomIn');
document.getElementById('zoom-out').title = t('zoomOut');

Expand Down Expand Up @@ -658,6 +666,15 @@ function newFile(initialContent) {
input.select();
}

function openExamplesNotebook() {
newFile(getDefaultContent());
const input = document.getElementById('new-file-input');
input.value = t('examplesFileName');
input.select();
}

document.getElementById('examples-btn').addEventListener('click', openExamplesNotebook);

function confirmNewFile() {
const input = document.getElementById('new-file-input');
const name = input.value.trim();
Expand Down
56 changes: 46 additions & 10 deletions src/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,45 @@ const PRECACHE_ASSETS = __PRECACHE_ASSETS__;
const APP_SHELL = new URL('./index.html', self.registration.scope).toString();

const scopedUrl = path => new URL(path, self.registration.scope).toString();
const NETWORK_FIRST_PATHS = new Set(
['./app.js', './app.css', './manifest.webmanifest'].map(
path => new URL(path, self.registration.scope).pathname
)
);

async function networkFirst(request, fallbackUrl = null) {
try {
const response = await fetch(request);
if (!response || !response.ok) throw new Error('Network response was not OK');

const cache = await caches.open(CACHE_NAME);
await cache.put(request, response.clone());
return response;
} catch {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request, { ignoreSearch: true });
if (cached) return cached;

if (fallbackUrl) {
const fallback = await cache.match(fallbackUrl);
if (fallback) return fallback;
}

return Response.error();
}
}

async function cacheFirst(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request, { ignoreSearch: true });
if (cached) return cached;

const response = await fetch(request);
if (response && response.ok) {
await cache.put(request, response.clone());
}
return response;
}

self.addEventListener('install', event => {
event.waitUntil(
Expand Down Expand Up @@ -37,17 +76,14 @@ self.addEventListener('fetch', event => {
if (url.origin !== self.location.origin) return;

if (request.mode === 'navigate') {
event.respondWith(
fetch(request)
.then(response => response)
.catch(() => caches.match(APP_SHELL))
);
event.respondWith(networkFirst(request, APP_SHELL));
return;
}

event.respondWith(
caches
.match(request, { ignoreSearch: true })
.then(cached => cached || fetch(request))
);
if (NETWORK_FIRST_PATHS.has(url.pathname)) {
event.respondWith(networkFirst(request));
return;
}

event.respondWith(cacheFirst(request));
});
Loading