refactor: updated login screen and favicon
All checks were successful
deploy / deploy (push) Successful in 4m50s

This commit is contained in:
Aswin B. S 2026-11-05 15:33:48 +05:30
parent 749d18192a
commit 32bdca2ef9
21 changed files with 288 additions and 81 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
assets/images/logo_coin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

View File

@ -205,41 +205,22 @@ class _BrandPanel extends StatelessWidget {
),
child: Stack(
children: [
Positioned(
right: large ? -120 : -50,
top: large ? -80 : -30,
child: _ring(large ? 420 : 190, C.onNavy8),
// A wall of ticket endings with a few gold ones, calmed in the
// middle so the coin and name stay clear. Decorative only.
Positioned.fill(
child: Image.asset(
'assets/images/login_cover.jpg',
fit: BoxFit.cover,
excludeFromSemantics: true,
errorBuilder: (_, _, _) => const SizedBox.shrink(),
),
Positioned(
left: large ? -90 : -40,
bottom: large ? -60 : 10,
child: _ring(large ? 300 : 120, C.onNavy5),
),
SafeArea(
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: coin,
height: coin,
decoration: const BoxDecoration(
color: C.gold,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: C.goldGlow,
blurRadius: 30,
offset: Offset(0, 10),
),
],
),
child: Icon(
Icons.confirmation_number_rounded,
color: C.navy,
size: coin * 0.48,
),
),
BrandCoin(size: coin),
SizedBox(height: large ? S.s24 : S.s18),
Text(
'SRS',
@ -247,7 +228,7 @@ class _BrandPanel extends StatelessWidget {
),
const SizedBox(height: S.s6),
Text(
'Your sequences, at a glance',
'Your ticket sequences, at a glance',
style: ts(large ? 16 : 14, c: C.softOnNavy),
),
],
@ -258,10 +239,4 @@ class _BrandPanel extends StatelessWidget {
),
);
}
Widget _ring(double size, Color color) => Container(
width: size,
height: size,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
);
}

View File

@ -7,7 +7,10 @@ import 'core/api.dart';
import 'core/config.dart';
import 'core/format.dart';
import 'core/http_api.dart';
import 'core/models.dart';
import 'core/providers.dart';
import 'core/theme.dart';
import 'widgets/ui.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -15,18 +18,56 @@ Future<void> main() async {
baseUrl: AppConfig.apiBaseUrl,
prefs: await SharedPreferences.getInstance(),
);
runApp(_Boot(api));
}
final user = await api.restore();
final start = dateOnly(api.now);
class _Boot extends StatefulWidget {
const _Boot(this.api);
final Api api;
runApp(
ProviderScope(
@override
State<_Boot> createState() => _BootState();
}
class _BootState extends State<_Boot> {
late final Future<AppUser?> _user = widget.api.restore().catchError(
(Object _) => null,
);
@override
Widget build(BuildContext context) => FutureBuilder<AppUser?>(
future: _user,
builder: (context, snap) {
if (snap.connectionState != ConnectionState.done) {
return const _Splash();
}
return ProviderScope(
overrides: [
apiProvider.overrideWithValue(api),
startDateProvider.overrideWithValue(start),
sessionProvider.overrideWith(() => SessionNotifier(user)),
apiProvider.overrideWithValue(widget.api),
startDateProvider.overrideWithValue(dateOnly(widget.api.now)),
sessionProvider.overrideWith(() => SessionNotifier(snap.data)),
],
child: const SrsApp(),
);
},
);
}
class _Splash extends StatelessWidget {
const _Splash();
@override
Widget build(BuildContext context) => Directionality(
textDirection: TextDirection.ltr,
child: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [C.navy, C.navyDeep],
),
),
child: const Center(child: BrandCoin(size: 96)),
),
);
}

View File

@ -237,16 +237,7 @@ class _Brand extends StatelessWidget {
@override
Widget build(BuildContext context) {
final coin = Container(
width: 40,
height: 40,
decoration: const BoxDecoration(color: C.gold, shape: BoxShape.circle),
child: const Icon(
Icons.confirmation_number_rounded,
color: C.navy,
size: 20,
),
);
const coin = BrandCoin(size: 40, glow: false);
if (!extended) return coin;
return Row(
children: [

View File

@ -1400,3 +1400,32 @@ class DateStrip extends ConsumerWidget {
);
}
}
class BrandCoin extends StatelessWidget {
const BrandCoin({super.key, required this.size, this.glow = true});
final double size;
final bool glow;
@override
Widget build(BuildContext context) => DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: glow
? [
BoxShadow(
color: C.goldGlow,
blurRadius: size * 0.36,
offset: Offset(0, size * 0.12),
),
]
: null,
),
child: Image.asset(
'assets/images/logo_coin.png',
width: size,
height: size,
excludeFromSemantics: true,
filterQuality: FilterQuality.medium,
),
);
}

View File

@ -60,6 +60,8 @@ flutter:
uses-material-design: true
assets:
- assets/icons/whatsapp.svg
- assets/images/login_cover.jpg
- assets/images/logo_coin.png
fonts:
- family: Inter
fonts:

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

View File

@ -0,0 +1,46 @@
import random, math
from PIL import Image, ImageDraw, ImageFilter, ImageFont, ImageChops
import sys, os; sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from make_icons import gradient, NAVY, DEEP, GOLD, FONT
W,H=1600,1600 # square: crops cleanly into the tall left panel and the phone banner
random.seed(74)
base=gradient((W,H),NAVY,DEEP).convert("RGBA")
# Wall of ticket endings, tilted, in two quiet tones with a few gold ones.
WW=2800
wall=Image.new("RGBA",(WW,WW),(0,0,0,0)); d=ImageDraw.Draw(wall)
big=ImageFont.truetype(FONT,138)
cw,ch=440,230
cols,rows=WW//cw+1,WW//ch+1
gold_cells={(2,3),(5,5),(3,8),(6,2),(1,10),(4,6)}
for r in range(rows):
for c in range(cols):
txt="%04d"%random.randint(0,9999)
x=c*cw+(cw//2 if r%2 else 0)-80; y=r*ch-40
if (c,r) in gold_cells:
fill=GOLD+(215,)
else:
fill=(255,255,255,20 if (c+r)%3 else 32)
d.text((x,y),txt,font=big,fill=fill)
wall=wall.rotate(-12,resample=Image.BICUBIC)
left=(WW-W)//2; top=(WW-H)//2
wall=wall.crop((left,top,left+W,top+H))
# Calm the middle, where the coin and wordmark sit, and darken the edges.
mask=Image.new("L",(W,H),255); md=ImageDraw.Draw(mask)
md.ellipse([W*0.08,H*0.18,W*0.92,H*0.82],fill=14)
mask=mask.filter(ImageFilter.GaussianBlur(200))
wall.putalpha(ImageChops.multiply(wall.getchannel("A"),mask))
img=Image.alpha_composite(base,wall)
vig=Image.new("L",(W,H),0); vd=ImageDraw.Draw(vig)
vd.ellipse([-W*0.15,-H*0.15,W*1.15,H*1.15],fill=255)
vig=vig.filter(ImageFilter.GaussianBlur(220))
dark=Image.new("RGBA",(W,H),DEEP+(255,))
dark.putalpha(Image.eval(vig,lambda v:int((255-v)*0.75)))
img=Image.alpha_composite(img,dark).convert("RGB")
out="C:/Project-1/srs/assets/images/login_cover.jpg"
import os; os.makedirs(os.path.dirname(out),exist_ok=True)
img.save(out,quality=84,optimize=True,progressive=True)
print(out,img.size,os.path.getsize(out)//1024,"KB")

View File

@ -0,0 +1,116 @@
import os, random, sys
from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
NAVY = (0x1B, 0x2A, 0x49); DEEP = (0x10, 0x1B, 0x33); GOLD = (0xD4, 0xA6, 0x4A)
FONT = os.path.join(ROOT, "assets", "fonts", "Inter-Bold.otf")
COIN = Image.open(os.path.join(HERE, "logo_coin_master.png")).convert("RGBA")
def gradient(size, c1, c2):
w, h = size
g = Image.new("RGB", (w, h))
px = g.load()
for y in range(h):
for x in range(w):
t = (x / w + y / h) / 2
px[x, y] = tuple(round(c1[i] + (c2[i] - c1[i]) * t) for i in range(3))
return g
def background(S, style):
bg = gradient((S, S), NAVY, DEEP).convert("RGBA")
ov = Image.new("RGBA", (S, S), (0, 0, 0, 0))
d = ImageDraw.Draw(ov)
if style == "circles":
# Large soft discs and one thin gold ring, partly off the tile.
d.ellipse([S * 0.46, -S * 0.42, S * 1.34, S * 0.46], fill=(255, 255, 255, 20))
d.ellipse([-S * 0.42, S * 0.52, S * 0.42, S * 1.36], fill=(255, 255, 255, 14))
d.ellipse([S * 0.58, S * 0.60, S * 1.18, S * 1.20], fill=(212, 166, 74, 22))
d.ellipse([S * 0.04, S * 0.04, S * 0.96, S * 0.96],
outline=(212, 166, 74, 46), width=max(1, round(S * 0.006)))
elif style == "endings":
rnd = random.Random(74)
f = ImageFont.truetype(FONT, round(S * 0.115))
cw, ch = S * 0.36, S * 0.19
gold = {(1, 1), (3, 3), (0, 4), (2, 5)}
layer = Image.new("RGBA", (S * 2, S * 2), (0, 0, 0, 0))
ld = ImageDraw.Draw(layer)
for r in range(int(S * 2 / ch) + 1):
for c in range(int(S * 2 / cw) + 1):
t = "%04d" % rnd.randint(0, 9999)
x = c * cw + (cw / 2 if r % 2 else 0) - S * 0.2
y = r * ch - S * 0.1
fill = GOLD + (150,) if (c, r) in gold else (255, 255, 255, 26)
ld.text((x, y), t, font=f, fill=fill)
layer = layer.rotate(-12, resample=Image.BICUBIC)
o = S // 2
layer = layer.crop((o, o, o + S, o + S))
ov = layer
return Image.alpha_composite(bg, ov)
def icon(px, style, ratio, shape, ss=4):
S = px * ss
bg = background(S, style)
cx = cy = S / 2
R = S * ratio / 2
if style == "endings":
# Calm the middle so the coin reads cleanly.
m = Image.new("L", (S, S), 255)
ImageDraw.Draw(m).ellipse([cx - R * 1.55, cy - R * 1.55, cx + R * 1.55, cy + R * 1.55], fill=70)
m = m.filter(ImageFilter.GaussianBlur(S * 0.07))
veil = Image.new("RGBA", (S, S), NAVY + (255,))
veil.putalpha(Image.eval(m, lambda v: 255 - v))
bg = Image.alpha_composite(bg, veil)
glow = Image.new("RGBA", (S, S), (0, 0, 0, 0))
ImageDraw.Draw(glow).ellipse([cx - R, cy - R + S * 0.02, cx + R, cy + R + S * 0.02], fill=GOLD + (130,))
glow = glow.filter(ImageFilter.GaussianBlur(S * 0.035))
bg = Image.alpha_composite(bg, glow)
coin = COIN.resize((round(R * 2), round(R * 2)), Image.LANCZOS)
bg.alpha_composite(coin, (round(cx - R), round(cy - R)))
if shape == "rounded":
mask = Image.new("L", (S, S), 0)
ImageDraw.Draw(mask).rounded_rectangle([0, 0, S - 1, S - 1], radius=S * 0.22, fill=255)
bg.putalpha(mask)
return bg.resize((px, px), Image.LANCZOS)
def write_icons(style):
web = os.path.join(ROOT, "web")
jobs = [
("icons/Icon-512.png", 512, 0.62, "rounded"),
("icons/Icon-192.png", 192, 0.62, "rounded"),
("icons/Icon-maskable-512.png", 512, 0.52, "full"),
("icons/Icon-maskable-192.png", 192, 0.52, "full"),
("icons/apple-touch-icon.png", 180, 0.62, "full"),
("favicon.png", 64, 0.78, "rounded"),
]
for name, px, ratio, shape in jobs:
icon(px, style, ratio, shape).save(os.path.join(web, name), optimize=True)
print(name, px)
def write_preview():
out = os.path.join(HERE, "preview")
os.makedirs(out, exist_ok=True)
for style in ("endings", "circles"):
sheet = Image.new("RGB", (512 + 40 + 192 + 40 + 64 + 40 + 32, 512 + 40), (244, 242, 236))
big = icon(512, style, 0.62, "rounded")
sheet.paste(big, (20, 20), big)
mid = icon(192, style, 0.62, "rounded")
sheet.paste(mid, (512 + 40, 20), mid)
small = icon(64, style, 0.78, "rounded")
sheet.paste(small, (512 + 40 + 192 + 20, 20), small)
tiny = icon(32, style, 0.78, "rounded")
sheet.paste(tiny, (512 + 40 + 192 + 20 + 64 + 20, 20), tiny)
sheet.save(os.path.join(out, f"icon_{style}.png"))
print("preview", style)
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] == "preview":
write_preview()
else:
write_icons(sys.argv[1] if len(sys.argv) > 1 else "circles")

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
web/icons/coin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

View File

@ -1,30 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="Your sequences, at a glance">
<meta name="description" content="Your ticket sequences, at a glance">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="SRS">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<link rel="apple-touch-icon" href="icons/apple-touch-icon.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
@ -32,16 +19,36 @@
<meta name="theme-color" content="#1B2A49">
<title>SRS</title>
<link rel="manifest" href="manifest.json">
<style>
html, body { margin: 0; height: 100%; background: #1B2A49; }
#splash {
position: fixed; inset: 0; display: flex; align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1B2A49, #101B33);
transition: opacity .2s ease-out;
}
#splash .coin {
width: 96px; height: 96px; border-radius: 50%;
box-shadow: 0 12px 34px rgba(212, 166, 74, .33);
}
@media (prefers-reduced-motion: no-preference) {
#splash .coin { animation: breathe 1.8s ease-in-out infinite; }
}
@keyframes breathe { 50% { transform: scale(.94); } }
</style>
</head>
<body>
<!--
You can customize the "flutter_bootstrap.js" script.
This is useful to provide a custom configuration to the Flutter loader
or to give the user feedback during the initialization process.
For more details:
* https://docs.flutter.dev/platform-integration/web/initialization
-->
<div id="splash" aria-hidden="true">
<img class="coin" src="icons/coin.png" width="96" height="96" alt="">
</div>
<script>
window.addEventListener('flutter-first-frame', function () {
var el = document.getElementById('splash');
if (!el) return;
el.style.opacity = '0';
setTimeout(function () { el.remove(); }, 250);
});
</script>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

View File

@ -3,9 +3,9 @@
"short_name": "SRS",
"start_url": ".",
"display": "standalone",
"background_color": "#F4F2EC",
"background_color": "#1B2A49",
"theme_color": "#1B2A49",
"description": "Your sequences, at a glance",
"description": "Your ticket sequences, at a glance",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [