Initial commit: Tagesbyte prototype
Flutter app that collects daily tech news from German and English RSS/Atom feeds, with a reader view, AI article summaries and daily briefings (Claude API or n8n webhook), paywall/sponsored filtering and ad/consent blocking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No files matched your search
|
After Width: | Height: | Size: 215 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1000" height="1000">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#5873FF"/>
|
||||
<stop offset="1" stop-color="#2231A6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1000" height="1000" fill="url(#bg)"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1000" height="1000">
|
||||
<defs>
|
||||
|
||||
<mask id="bite" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000">
|
||||
<rect width="1000" height="1000" fill="#fff"/>
|
||||
<circle cx="692.0188835160362" cy="274.5048250192058" r="88" fill="#000"/><circle cx="665.4951749807942" cy="247.98111648396377" r="88" fill="#000"/>
|
||||
</mask>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="16"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(500 500) scale(0.9) translate(-500 -500)">
|
||||
<!-- Blur after cutting the bite so the shadow follows the bitten edge. -->
|
||||
<g transform="translate(0 24)" filter="url(#shadow)" opacity="0.35">
|
||||
<g mask="url(#bite)"><rect x="290" y="230" width="420" height="540" rx="52" fill="#0B1460" /></g>
|
||||
</g>
|
||||
<g mask="url(#bite)"><rect x="290" y="230" width="420" height="540" rx="52" fill="#FFFFFF" /></g>
|
||||
|
||||
<rect x="345" y="335" width="215" height="50" rx="25" fill="#3355DD"/>
|
||||
<rect x="345" y="420" width="155" height="50" rx="25" fill="#3355DD"/>
|
||||
<rect x="345" y="525" width="310" height="28" rx="14" fill="#C9D3F7"/>
|
||||
<rect x="345" y="588" width="310" height="28" rx="14" fill="#C9D3F7"/>
|
||||
<rect x="345" y="651" width="200" height="28" rx="14" fill="#C9D3F7"/>
|
||||
<path d="M615,619 C620.52,659.48 620.52,659.48 661,665 C620.52,670.52 620.52,670.52 615,711 C609.48,670.52 609.48,670.52 569,665 C609.48,659.48 609.48,659.48 615,619 Z" fill="#FFB52E"/>
|
||||
</g></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 221 KiB |
@@ -0,0 +1,32 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1000" height="1000">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#5873FF"/>
|
||||
<stop offset="1" stop-color="#2231A6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1000" height="1000" fill="url(#bg)"/>
|
||||
<defs>
|
||||
|
||||
<mask id="bite" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000">
|
||||
<rect width="1000" height="1000" fill="#fff"/>
|
||||
<circle cx="692.0188835160362" cy="274.5048250192058" r="88" fill="#000"/><circle cx="665.4951749807942" cy="247.98111648396377" r="88" fill="#000"/>
|
||||
</mask>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="16"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="translate(500 500) scale(1.1) translate(-500 -500)">
|
||||
<!-- Blur after cutting the bite so the shadow follows the bitten edge. -->
|
||||
<g transform="translate(0 24)" filter="url(#shadow)" opacity="0.35">
|
||||
<g mask="url(#bite)"><rect x="290" y="230" width="420" height="540" rx="52" fill="#0B1460" /></g>
|
||||
</g>
|
||||
<g mask="url(#bite)"><rect x="290" y="230" width="420" height="540" rx="52" fill="#FFFFFF" /></g>
|
||||
|
||||
<rect x="345" y="335" width="215" height="50" rx="25" fill="#3355DD"/>
|
||||
<rect x="345" y="420" width="155" height="50" rx="25" fill="#3355DD"/>
|
||||
<rect x="345" y="525" width="310" height="28" rx="14" fill="#C9D3F7"/>
|
||||
<rect x="345" y="588" width="310" height="28" rx="14" fill="#C9D3F7"/>
|
||||
<rect x="345" y="651" width="200" height="28" rx="14" fill="#C9D3F7"/>
|
||||
<path d="M615,619 C620.52,659.48 620.52,659.48 661,665 C620.52,670.52 620.52,670.52 615,711 C609.48,670.52 609.48,670.52 569,665 C609.48,659.48 609.48,659.48 615,619 Z" fill="#FFB52E"/>
|
||||
</g></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,123 @@
|
||||
// Generates the Tagesbyte app icon sources (SVG) and PNG renders.
|
||||
// Usage: npm install @resvg/resvg-js@2.6.2 && node make_icons.js assets/icon
|
||||
// then: dart run flutter_launcher_icons
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { Resvg } = require('@resvg/resvg-js');
|
||||
|
||||
const out = process.argv[2];
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
|
||||
const BLUE = '#3355DD';
|
||||
const LINE = '#C9D3F7';
|
||||
const SPARK = '#FFB52E';
|
||||
|
||||
// Design space 1000x1000. A news page with a bite out of its top-right corner.
|
||||
const page = { x: 290, y: 230, w: 420, h: 540, r: 52 };
|
||||
const corner = { x: page.x + page.w, y: page.y };
|
||||
// Two overlapping circles: together they remove the corner and leave a small
|
||||
// cusp between them, which reads as a bite mark.
|
||||
const biteCircles = [112, 158].map((deg) => {
|
||||
const a = (deg * Math.PI) / 180;
|
||||
return { cx: corner.x + 48 * Math.cos(a), cy: corner.y + 48 * Math.sin(a), r: 88 };
|
||||
});
|
||||
|
||||
const sparkle = (cx, cy, s) =>
|
||||
`M${cx},${cy - s} C${cx + s * 0.12},${cy - s * 0.12} ${cx + s * 0.12},${cy - s * 0.12} ${cx + s},${cy} ` +
|
||||
`C${cx + s * 0.12},${cy + s * 0.12} ${cx + s * 0.12},${cy + s * 0.12} ${cx},${cy + s} ` +
|
||||
`C${cx - s * 0.12},${cy + s * 0.12} ${cx - s * 0.12},${cy + s * 0.12} ${cx - s},${cy} ` +
|
||||
`C${cx - s * 0.12},${cy - s * 0.12} ${cx - s * 0.12},${cy - s * 0.12} ${cx},${cy - s} Z`;
|
||||
|
||||
const pageRect = (fill, extra = '') =>
|
||||
`<rect x="${page.x}" y="${page.y}" width="${page.w}" height="${page.h}" rx="${page.r}" fill="${fill}" ${extra}/>`;
|
||||
|
||||
const biteMask = (id) => `
|
||||
<mask id="${id}" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000">
|
||||
<rect width="1000" height="1000" fill="#fff"/>
|
||||
${biteCircles.map((c) => `<circle cx="${c.cx}" cy="${c.cy}" r="${c.r}" fill="#000"/>`).join('')}
|
||||
</mask>`;
|
||||
|
||||
// Headline bar, text lines and the AI sparkle on the page.
|
||||
const contentShapes = (headline, line, spark) => `
|
||||
<rect x="345" y="335" width="215" height="50" rx="25" fill="${headline}"/>
|
||||
<rect x="345" y="420" width="155" height="50" rx="25" fill="${headline}"/>
|
||||
<rect x="345" y="525" width="310" height="28" rx="14" fill="${line}"/>
|
||||
<rect x="345" y="588" width="310" height="28" rx="14" fill="${line}"/>
|
||||
<rect x="345" y="651" width="200" height="28" rx="14" fill="${line}"/>
|
||||
<path d="${sparkle(615, 665, 46)}" fill="${spark}"/>`;
|
||||
|
||||
function foreground(scale) {
|
||||
const t = `translate(500 500) scale(${scale}) translate(-500 -500)`;
|
||||
return `
|
||||
<defs>
|
||||
${biteMask('bite')}
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feGaussianBlur stdDeviation="16"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g transform="${t}">
|
||||
<!-- Blur after cutting the bite so the shadow follows the bitten edge. -->
|
||||
<g transform="translate(0 24)" filter="url(#shadow)" opacity="0.35">
|
||||
<g mask="url(#bite)">${pageRect('#0B1460')}</g>
|
||||
</g>
|
||||
<g mask="url(#bite)">${pageRect('#FFFFFF')}</g>
|
||||
${contentShapes(BLUE, LINE, SPARK)}
|
||||
</g>`;
|
||||
}
|
||||
|
||||
const background = `
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#5873FF"/>
|
||||
<stop offset="1" stop-color="#2231A6"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="1000" height="1000" fill="url(#bg)"/>`;
|
||||
|
||||
const svg = (body) =>
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1000" height="1000">${body}</svg>`;
|
||||
|
||||
// Monochrome (Android 13 themed icons): page silhouette, content cut out.
|
||||
const monochrome = svg(`
|
||||
<defs>
|
||||
${biteMask('bite')}
|
||||
<mask id="content" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000">
|
||||
<rect width="1000" height="1000" fill="#fff"/>
|
||||
${contentShapes('#000', '#000', '#000')}
|
||||
</mask>
|
||||
</defs>
|
||||
<g transform="translate(500 500) scale(0.9) translate(-500 -500)">
|
||||
<g mask="url(#bite)"><g mask="url(#content)">${pageRect('#FFFFFF')}</g></g>
|
||||
</g>`);
|
||||
|
||||
const files = {
|
||||
// Full-bleed icon for iOS, web and Windows (the OS rounds the corners).
|
||||
'icon.svg': svg(background + foreground(1.1)),
|
||||
// Android adaptive icon layers: content kept inside the 66dp safe zone.
|
||||
'foreground.svg': svg(foreground(0.9)),
|
||||
'background.svg': svg(background),
|
||||
'monochrome.svg': monochrome,
|
||||
};
|
||||
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
fs.writeFileSync(path.join(out, name), content);
|
||||
const png = new Resvg(content, { fitTo: { mode: 'width', value: 1024 } }).render().asPng();
|
||||
fs.writeFileSync(path.join(out, name.replace('.svg', '.png')), png);
|
||||
console.log('wrote', name, 'and', name.replace('.svg', '.png'));
|
||||
}
|
||||
|
||||
// Preview: the full icon at launcher size plus a circle-masked adaptive icon.
|
||||
const preview = svg(`
|
||||
<defs><clipPath id="c"><circle cx="500" cy="500" r="500"/></clipPath></defs>
|
||||
<g clip-path="url(#c)">${background}${foreground(0.9)}</g>`);
|
||||
fs.writeFileSync(path.join(out, '_preview_adaptive.png'),
|
||||
new Resvg(preview, { fitTo: { mode: 'width', value: 512 } }).render().asPng());
|
||||
|
||||
// Monochrome preview on a dark background (the real file is transparent), and
|
||||
// the full icon at a real launcher size to check it still reads when small.
|
||||
const inner = (s) => s.replace(/^<svg[^>]*>/, '').replace(/<\/svg>$/, '');
|
||||
fs.writeFileSync(path.join(out, '_preview_monochrome.png'),
|
||||
new Resvg(svg('<rect width="1000" height="1000" fill="#333"/>' + inner(monochrome)),
|
||||
{ fitTo: { mode: 'width', value: 256 } }).render().asPng());
|
||||
fs.writeFileSync(path.join(out, '_preview_small.png'),
|
||||
new Resvg(files['icon.svg'], { fitTo: { mode: 'width', value: 96 } }).render().asPng());
|
||||
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,21 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000" width="1000" height="1000">
|
||||
<defs>
|
||||
|
||||
<mask id="bite" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000">
|
||||
<rect width="1000" height="1000" fill="#fff"/>
|
||||
<circle cx="692.0188835160362" cy="274.5048250192058" r="88" fill="#000"/><circle cx="665.4951749807942" cy="247.98111648396377" r="88" fill="#000"/>
|
||||
</mask>
|
||||
<mask id="content" maskUnits="userSpaceOnUse" x="0" y="0" width="1000" height="1000">
|
||||
<rect width="1000" height="1000" fill="#fff"/>
|
||||
|
||||
<rect x="345" y="335" width="215" height="50" rx="25" fill="#000"/>
|
||||
<rect x="345" y="420" width="155" height="50" rx="25" fill="#000"/>
|
||||
<rect x="345" y="525" width="310" height="28" rx="14" fill="#000"/>
|
||||
<rect x="345" y="588" width="310" height="28" rx="14" fill="#000"/>
|
||||
<rect x="345" y="651" width="200" height="28" rx="14" fill="#000"/>
|
||||
<path d="M615,619 C620.52,659.48 620.52,659.48 661,665 C620.52,670.52 620.52,670.52 615,711 C609.48,670.52 609.48,670.52 569,665 C609.48,659.48 609.48,659.48 615,619 Z" fill="#000"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<g transform="translate(500 500) scale(0.9) translate(-500 -500)">
|
||||
<g mask="url(#bite)"><g mask="url(#content)"><rect x="290" y="230" width="420" height="540" rx="52" fill="#FFFFFF" /></g></g>
|
||||
</g></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |