<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Muhammed Said Çakır</title>
    <description>The latest articles on DEV Community by Muhammed Said Çakır (@muhammedsaidckr).</description>
    <link>https://dev.to/muhammedsaidckr</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1050563%2F396b45d4-3064-4768-9e14-3c2ec037931a.jpeg</url>
      <title>DEV Community: Muhammed Said Çakır</title>
      <link>https://dev.to/muhammedsaidckr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/muhammedsaidckr"/>
    <language>en</language>
    <item>
      <title>Building an offline snag-list app with Expo, Skia and pdf-lib</title>
      <dc:creator>Muhammed Said Çakır</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:23:38 +0000</pubDate>
      <link>https://dev.to/muhammedsaidckr/building-an-offline-snag-list-app-with-expo-skia-and-pdf-lib-4hef</link>
      <guid>https://dev.to/muhammedsaidckr/building-an-offline-snag-list-app-with-expo-skia-and-pdf-lib-4hef</guid>
      <description>&lt;p&gt;A snag list (a punch list in the US) is the walk-round at the end of a job: you photograph every defect, mark it up, note where it is, and send the builder one numbered report. Electricians do it for clients, site managers do it at handover, and new-home buyers in the UK do it before the builder's warranty clock runs out.&lt;/p&gt;

&lt;p&gt;The apps for this split into two camps. Some are team platforms with an account, a cloud project and a monthly price per user. Most of the others are free to install and charge for the one thing you actually need, the PDF. I wanted a third kind: pay once, no account, and it works in a basement with no signal. That became PunchSheet. This post is about the parts that were harder than they look.&lt;/p&gt;

&lt;h2&gt;
  
  
  No network, enforced
&lt;/h2&gt;

&lt;p&gt;"Works offline" is easy to say and easy to break later, so the rule is mechanical:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Android release build has no &lt;code&gt;INTERNET&lt;/code&gt; permission. A config plugin removes it only from the &lt;code&gt;release&lt;/code&gt; source set, so debug builds can still reach Metro.&lt;/li&gt;
&lt;li&gt;The build script dumps the APK's permissions and compares them with an exact list. An extra permission fails the build.&lt;/li&gt;
&lt;li&gt;A repository rule rejects &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;XMLHttpRequest&lt;/code&gt; and &lt;code&gt;WebSocket&lt;/code&gt; in the app's source.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Projects live in SQLite (&lt;code&gt;expo-sqlite&lt;/code&gt;), photos in the app's own files. There is nothing to sign in to and nothing to sync, which also makes the privacy policy short: we receive nothing from the app.&lt;/p&gt;

&lt;h2&gt;
  
  
  Photos stay original; markup is data
&lt;/h2&gt;

&lt;p&gt;Each snag can hold several photos. The original file is never changed. Markup (arrows, circles, rectangles, lines, freehand, text) is stored as vector shapes and drawn with React Native Skia.&lt;/p&gt;

&lt;p&gt;The PDF never uses the original. When a photo is added, the app writes a report copy: long edge 1,600 px, JPEG quality 0.8, with the markup flattened onto it through Skia. The report is then built from those copies.&lt;/p&gt;

&lt;p&gt;That decision came from a spike on a real phone before any screens existed. Building a 100-photo report from 12 MP test images, with the shrunken JPEGs held in memory, peaked at 1.1 GB. Streaming them one by one brought the peak down to 764 MB, still too much for a mid-range phone. Doing the expensive decode once, at add time, is what made the report fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 15 MB budget for the PDF
&lt;/h2&gt;

&lt;p&gt;A report that email refuses is useless on site, so the PDF has a budget: 15,000,000 bytes, the decimal reading of common 15 MB limits.&lt;/p&gt;

&lt;p&gt;The PDF is assembled on the phone with pdf-lib, in plain JS. Each photo goes through a ladder of encodings, from 1,600 px down to 1,200 px and quality 0.7 down to 0.5. Walking the ladder photo by photo meant re-encoding the same image many times. Instead, the app predicts the starting rung from size ratios measured on real photos, and the budget is shared across the whole report.&lt;/p&gt;

&lt;p&gt;With 100 downloaded test photos, the prediction cut the build from 32 to 7 seconds and from 596 encodes to 89. On a Nothing Phone (2), a project with 50 snags and 100 camera photos now gives a 26-page report of 14.08 MB in about 11 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pure core, tested in Node
&lt;/h2&gt;

&lt;p&gt;Everything that isn't UI is plain TypeScript: numbering, the report model, the size budget, the backup format, EXIF orientation, markup geometry. It runs under &lt;code&gt;node --test --experimental-strip-types&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The storage layer talks to a small &lt;code&gt;Db&lt;/code&gt; interface. On the phone that is &lt;code&gt;expo-sqlite&lt;/code&gt;; in tests it is Node's built-in &lt;code&gt;node:sqlite&lt;/code&gt;, so the repository tests use a real SQLite database without a device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups you can carry
&lt;/h2&gt;

&lt;p&gt;With no cloud, the backup is the user's safety net. A project exports to one &lt;code&gt;.punchsheet&lt;/code&gt; file (a zip made with fflate) with every photo and all markup, and imports on another phone, Android or iPhone.&lt;/p&gt;

&lt;p&gt;The first import read the file in one go. With a 211 MB backup, &lt;code&gt;File.bytes()&lt;/code&gt; hit the Java heap's 256 MB limit and threw &lt;code&gt;OutOfMemoryError&lt;/code&gt;. Import now streams the file in 1 MB chunks and keeps one photo in memory at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small things that cost an evening each
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;R8 renames exception classes.&lt;/strong&gt; Expo derives an error code from the Kotlin class name, and in a minified release build that name changes. Cancelling the folder picker started showing "could not be saved". The app now also checks the message ("cancelled by the user").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Android adds ".zip".&lt;/strong&gt; Writing a backup through the storage access framework with the &lt;code&gt;application/zip&lt;/code&gt; type made Android rename it. The file is written as &lt;code&gt;application/octet-stream&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iOS merges labelled Pressables.&lt;/strong&gt; A dialog whose backdrop was a labelled &lt;code&gt;Pressable&lt;/code&gt; wrapping the card became a single "Cancel" element in VoiceOver, and the Delete button was unreachable. The backdrop is now a sibling of the card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Play's own protection works offline.&lt;/strong&gt; Google Play adds a licence check to protected apps. On a phone in airplane mode, with the Play Store stopped, it still passed on a cold start.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where it is
&lt;/h2&gt;

&lt;p&gt;PunchSheet costs $9.99 once (each store sets its local price): no subscription, no in-app purchases, no ads. The UI is English, and the app calls them snags, punch items or defects, as you prefer.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Play: &lt;a href="https://play.google.com/store/apps/details?id=tr.msclabs.punchsheet" rel="noopener noreferrer"&gt;https://play.google.com/store/apps/details?id=tr.msclabs.punchsheet&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;App Store: coming soon (in review)&lt;/li&gt;
&lt;li&gt;More, and the privacy policy: &lt;a href="https://msclabs.tr/en/punchsheet" rel="noopener noreferrer"&gt;https://msclabs.tr/en/punchsheet&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built something offline-first with Expo, I'd like to hear what broke for you.&lt;/p&gt;

</description>
      <category>expo</category>
      <category>reactnative</category>
      <category>showdev</category>
      <category>android</category>
    </item>
    <item>
      <title>How to monitor Türkiye's Official Gazette (Resmî Gazete) automatically</title>
      <dc:creator>Muhammed Said Çakır</dc:creator>
      <pubDate>Tue, 27 Oct 2026 23:14:54 +0000</pubDate>
      <link>https://dev.to/muhammedsaidckr/how-to-monitor-turkiyes-official-gazette-resmi-gazete-automatically-mp9</link>
      <guid>https://dev.to/muhammedsaidckr/how-to-monitor-turkiyes-official-gazette-resmi-gazete-automatically-mp9</guid>
      <description>&lt;p&gt;Every law, regulation and presidential decision in Türkiye takes effect through one daily publication: the Resmî Gazete, the Official Gazette. If your work depends on Turkish rules (data protection, tax, customs, energy, labour), you need to know the day something changes, not the week after.&lt;/p&gt;

&lt;p&gt;This guide shows a way to get the relevant items by email or Slack each morning, filtered by your own keywords, without reading the whole issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the gazette publishes
&lt;/h2&gt;

&lt;p&gt;A new issue appears every day at midnight Türkiye time at &lt;a href="https://www.resmigazete.gov.tr" rel="noopener noreferrer"&gt;resmigazete.gov.tr&lt;/a&gt;. Its index lists each item with its section, its type (regulation, communiqué, presidential decision, board decision) and a link to the full text.&lt;/p&gt;

&lt;p&gt;Over 31 issues, from 28 September to 28 October 2026, the index listed 321 items, about ten a day. Of those:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;126 had "yönetmelik" (regulation) in the title or type;&lt;/li&gt;
&lt;li&gt;28 were communiqués ("tebliğ");&lt;/li&gt;
&lt;li&gt;39 mentioned energy, electricity or natural gas;&lt;/li&gt;
&lt;li&gt;4 were presidential decisions;&lt;/li&gt;
&lt;li&gt;2 were about personal data protection (KVKK);&lt;/li&gt;
&lt;li&gt;2 were about tax.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most topics appear a few times a month. That is the problem: the item you need is one line among three hundred, and it can come on any day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The manual way
&lt;/h2&gt;

&lt;p&gt;Open the daily index and read the titles. It takes a few minutes a day, and it fails the day you forget. Titles are in Turkish, so a non-Turkish speaker also needs to know which words to look for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The automatic way
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://apify.com/msclabs/resmi-gazete-monitor" rel="noopener noreferrer"&gt;Turkey Official Gazette (Resmî Gazete) Monitor&lt;/a&gt; on Apify reads the daily index and returns the items that contain your keywords, as structured data: date, section, type, title and a link to the full text. Disclosure: I built it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a free Apify account and open the Actor.&lt;/li&gt;
&lt;li&gt;Enter a few keywords and press &lt;strong&gt;Start&lt;/strong&gt; once to see the results.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Schedules&lt;/strong&gt;, run it every day, for example at 09:00 Europe/Istanbul.&lt;/li&gt;
&lt;li&gt;Under &lt;strong&gt;Integrations&lt;/strong&gt;, add "Send results email via Gmail", Slack or a webhook.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ready-made examples, each with a sample run of the last 31 days: &lt;a href="https://apify.com/msclabs/resmi-gazete-monitor/examples/turkey-data-protection-kvkk-regulation-alerts" rel="noopener noreferrer"&gt;data protection (KVKK)&lt;/a&gt;, &lt;a href="https://apify.com/msclabs/resmi-gazete-monitor/examples/turkey-tax-regulation-alerts-from-resmi-gazete" rel="noopener noreferrer"&gt;tax&lt;/a&gt;, &lt;a href="https://apify.com/msclabs/resmi-gazete-monitor/examples/turkey-customs-and-trade-regulation-alerts" rel="noopener noreferrer"&gt;customs and trade&lt;/a&gt;, &lt;a href="https://apify.com/msclabs/resmi-gazete-monitor/examples/turkey-energy-market-regulation-alerts" rel="noopener noreferrer"&gt;energy&lt;/a&gt;, &lt;a href="https://apify.com/msclabs/resmi-gazete-monitor/examples/turkey-presidential-decisions-from-resmi-gazete" rel="noopener noreferrer"&gt;presidential decisions&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing keywords
&lt;/h2&gt;

&lt;p&gt;Matching ignores case and Turkish accents, so &lt;code&gt;asgari ucret&lt;/code&gt; finds "Asgari Ücret" and &lt;code&gt;istanbul&lt;/code&gt; finds "İSTANBUL". A keyword matches any part of a title, which has two consequences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a short stem.&lt;/strong&gt; &lt;code&gt;kişisel verilerin korunması&lt;/code&gt; ("protection of personal data") misses the board's own name, "Kişisel Verileri Koruma Kurulu". &lt;code&gt;kişisel veri&lt;/code&gt; finds both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the first results.&lt;/strong&gt; A short stem can catch unrelated words: &lt;code&gt;çalışma&lt;/code&gt; ("work") also matches university research centres named "Çalışmalar".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Run it once over 31 days, look at what it found, then narrow the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;$0.005 for each issue read, whatever the number of matches. A daily watch reads one issue a day: about $0.15 a month, which Apify's free plan ($5 of usage a month) covers. A one-month look-back costs about $0.16.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It reads titles and types, not full texts.&lt;/li&gt;
&lt;li&gt;It is not an official service. Confirm each result on the linked official page.&lt;/li&gt;
&lt;li&gt;Under Article 31 of Turkey's Law on Intellectual and Artistic Works (No. 5846), officially published legislation and decisions may be freely reproduced. The Actor reads only the public index pages, one request per issue.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you track Turkish regulations for clients or for compliance, one daily email with three lines in it replaces reading three hundred.&lt;/p&gt;

</description>
      <category>turkey</category>
      <category>automation</category>
      <category>webscraping</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
