Store assets: landscape tablet screenshots (iPad, Android 7/10-inch)

Captured from the app's real sidebar shell at 1024x768; adds landscape
compositions to compose.py and the README upload table.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Ayushya Amitabh
2026-10-07 21:46:29 -04:00
co-authored by Claude Sonnet 5.5
parent 11b3d535a9
commit 5837464581
44 changed files with 288 additions and 19 deletions
@@ -11,6 +11,10 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:noo/main.dart' show MainShellView;
import 'package:noo/providers/recent_controller.dart';
import 'package:noo/providers/shares_controller.dart';
import 'package:noo/providers/trash_controller.dart';
import 'package:noo/models/app_tab.dart';
import 'package:noo/theme/design_tokens.dart';
import 'package:noo/views/lock_screen_view.dart';
@@ -169,10 +173,26 @@ class DemoOps extends ItemOperations {
Future<List<NextcloudSharee>> searchSharees(String query) async => [];
}
/// The shell collapses to the Offline tab alone when it thinks there is no
/// network, so the landscape (full shell) captures report an online device.
class DemoConnectivity extends ConnectivityController {
@override
bool get isOffline => false;
}
class DemoFiles extends FilesController {
DemoFiles(super.session, {this.external = false});
final bool external;
@override
NextcloudUserQuota? get quota => const NextcloudUserQuota(
usedBytes: 46000000000,
totalBytes: 107374182400,
usagePercentage: 0.43,
userName: 'alex',
email: 'alex@cloud.example.com',
serverVersion: '31.0.0',
);
@override
StorageScope get storageScope =>
external ? StorageScope.all : StorageScope.cloud;
@override
@@ -329,12 +349,31 @@ void main() {
MockStreamHandler.inline(onListen: (arguments, events) {}),
);
}
// The shell listens for share/pick intents; none ever arrive here.
for (final name in [
'dev.ayushya.noo/share_intent/new',
'dev.ayushya.noo/pick_intent/new',
]) {
messenger.setMockStreamHandler(
EventChannel(name),
MockStreamHandler.inline(onListen: (arguments, events) {}),
);
}
messenger.setMockStreamHandler(
const EventChannel('dev.fluttercommunity.plus/connectivity_status'),
MockStreamHandler.inline(onListen: (arguments, events) {}),
);
});
for (final device in ['android', 'atablet', 'iphone', 'ipad']) {
// `ipadl` / `atabletl` are the landscape tablet captures, which render the
// app's real shell (sidebar + toolbar) instead of the single-screen widgets.
for (final device in [
'android',
'atablet',
'atabletl',
'iphone',
'ipad',
'ipadl',
]) {
for (final screen in [
'accounts',
'lock',
@@ -353,17 +392,27 @@ void main() {
'ui_lock_account_switching': true,
'ui_lock_hidden_files': true,
'ui_use_dynamic_color': false,
// Skips the shell's one-time notification-permission prompt.
'notification_permission_asked': true,
});
final tablet = device == 'ipad' || device == 'atablet';
final android = device == 'android' || device == 'atablet';
final size = tablet
final landscape = device == 'ipadl' || device == 'atabletl';
final tablet = landscape || device == 'ipad' || device == 'atablet';
final android =
device == 'android' ||
device == 'atablet' ||
device == 'atabletl';
final size = landscape
? const Size(1024, 768)
: tablet
? const Size(768, 1024)
: (android ? const Size(430, 800) : const Size(430, 932));
tester.view.devicePixelRatio = 1;
tester.view.physicalSize = size;
await tester.binding.setSurfaceSize(size);
final key = GlobalKey();
final connectivity = ConnectivityController();
final connectivity = landscape
? DemoConnectivity()
: ConnectivityController();
final session = DemoSession(connectivity);
final files = DemoFiles(session, external: screen == 'external');
final settings = SettingsController();
@@ -378,6 +427,12 @@ void main() {
photos: photos,
favorites: favorites,
);
final trash = TrashController(session);
final shares = SharesController(session);
final recent = RecentController(session);
if (landscape && screen == 'offline') {
settings.requestTab(AppTab.offline);
}
final scroll = ScrollController();
final style = android ? NooNavStyle.android : NooNavStyle.ios;
final browsing = const {
@@ -412,6 +467,12 @@ void main() {
ChangeNotifierProvider<OfflineController>.value(value: offline),
ChangeNotifierProvider<PickController>.value(value: pick),
ChangeNotifierProvider<PhotosController>.value(value: photos),
ChangeNotifierProvider<FavoritesController>.value(
value: favorites,
),
ChangeNotifierProvider<TrashController>.value(value: trash),
ChangeNotifierProvider<SharesController>.value(value: shares),
ChangeNotifierProvider<RecentController>.value(value: recent),
Provider<ItemOperations>.value(value: ops),
],
child: MaterialApp(
@@ -423,6 +484,8 @@ void main() {
card ? child! : RepaintBoundary(key: key, child: child),
home: screen == 'lock'
? const LockScreenView()
: landscape && browsing
? const MainShellView()
: Scaffold(
body: browsing
? FilesView(
@@ -438,16 +501,27 @@ void main() {
)
: Align(
alignment: Alignment.topCenter,
child: Builder(
builder: (context) => RepaintBoundary(
key: key,
child: ColoredBox(
color: context.nooColors.bg,
child: Padding(
padding: const EdgeInsets.all(12),
child: screen == 'security'
? const SettingsSecuritySection()
: const SettingsFileSyncSection(),
child: ConstrainedBox(
// Landscape tablets would stretch the card
// across the whole window; keep it phone-like.
constraints: BoxConstraints(
maxWidth: landscape ? 520 : double.infinity,
),
child: Builder(
builder: (context) => RepaintBoundary(
key: key,
child: ColoredBox(
color: context.nooColors.bg,
child: Padding(
padding: const EdgeInsets.all(12),
// The desktop-style card fills its
// height, so size it to its content.
child: IntrinsicHeight(
child: screen == 'security'
? const SettingsSecuritySection()
: const SettingsFileSyncSection(),
),
),
),
),
),
@@ -514,6 +588,9 @@ void main() {
});
await tester.pumpWidget(const SizedBox.shrink());
scroll.dispose();
trash.dispose();
shares.dispose();
recent.dispose();
pick.dispose();
offline.dispose();
sync.dispose();
+52 -3
View File
@@ -31,7 +31,7 @@ STORIES = [
eyebrow='MULTIPLE ACCOUNTS',
lines=('Every cloud,', 'one tap apart.'),
sub='Personal, work and family servers together.'),
dict(key='locks', theme='dark', back='lock', front='security', card_at=.66,
dict(key='locks', theme='dark', back='lock', front='security', card_at=.66, card_at_l=.66,
eyebrow='LOCKS',
lines=('Locks where', 'you want them.'),
sub='Protect opening Noo, switching accounts and hidden files with your device PIN or biometrics.'),
@@ -56,6 +56,10 @@ TARGETS = [
('android/tablet-10in', 'atablet', (1600, 2560), 'tablet'),
('ios/iphone', 'iphone', (1290, 2796), 'phone'),
('ios/ipad', 'ipad', (2064, 2752), 'tablet'),
# Landscape tablets show the app's real sidebar layout.
('android/tablet-7in-landscape', 'atabletl', (1920, 1200), 'landscape'),
('android/tablet-10in-landscape', 'atabletl', (2560, 1600), 'landscape'),
('ios/ipad-landscape', 'ipadl', (2752, 2064), 'landscape'),
]
@@ -127,7 +131,51 @@ def paste_panel(canvas, source, box, radius, t, bleed=False):
return x, y, sw, sh
def landscape_canvas(story, size, device):
"""Text column on the left, the real tablet UI on the right."""
t = THEMES[story['theme']]
w, h = size
im = background(size, t)
d = ImageDraw.Draw(im)
margin = round(h * .07)
colw = round(w * .30)
y = round(h * .27)
spaced(d, (margin, y), story['eyebrow'], font(round(h * .022), 'Bold', 'InstrumentSans'),
t['accent_text'], h * .004)
y += round(h * .06)
head = round(h * .068)
for index, line in enumerate(story['lines']):
fnt, _ = fit(d, line, font, head, colw)
d.text((margin, y), line, font=fnt, fill=t['accent_text'] if index else t['fg'])
y += round(head * 1.12)
y += round(head * .2)
sub_size = round(h * .030)
fnt = body_font(sub_size, 'Regular')
for line in textwrap.wrap(story['sub'], max(16, int(colw / (sub_size * .52)))):
d.text((margin, y), line, font=fnt, fill=t['fg2'])
y += round(sub_size * 1.4)
x0 = margin + colw + round(w * .035)
src = lambda name: OUT / f'source/{device}/{name}.png'
back = Image.open(src(story['back']))
aw, ah = w - x0 - margin, h - margin * 2
factor = min(aw / back.width, ah / back.height)
# Centered vertically; a layered card hangs below the panel's bottom edge.
area = (x0, round((h - back.height * factor) / 2) - (round(h * .03) if story['front'] else 0), aw, ah)
radius = round(h * .03)
bx, by, bw, bh = paste_panel(im, src(story['back']), area, radius, t)
if story['front']:
card = content_crop(src(story['front']))
scale = bw / Image.open(src(story['back'])).width
cw, ch = round(card.width * scale), round(card.height * scale)
cx = bx + round(bw * .40)
cy = by + round(bh * story.get('card_at_l', story['card_at']))
paste_panel(im, card, (cx, cy, cw, ch), round(radius * .6), t)
return im
def story_canvas(story, size, kind, device):
if kind == 'landscape':
return landscape_canvas(story, size, device)
t = THEMES[story['theme']]
w, h = size
im = background(size, t)
@@ -218,11 +266,12 @@ for asset in manifest:
(OUT / 'manifest.json').write_text(json.dumps(manifest, indent=2) + '\n')
# Review materials (not for upload): contact sheet and a gallery page.
contact = Image.new('RGB', (1920, 1500), '#1c1b18')
contact = Image.new('RGB', (1920, 2160), '#1c1b18')
cd = ImageDraw.Draw(contact)
cd.text((32, 20), 'Noo / store listing artwork', font=font(28), fill='#F3EFE6')
rows = [('ANDROID PHONE', 'android/phone'), ('ANDROID 10" TABLET', 'android/tablet-10in'),
('IPHONE', 'ios/iphone'), ('IPAD', 'ios/ipad')]
('IPHONE', 'ios/iphone'), ('IPAD', 'ios/ipad'),
('ANDROID 10" LANDSCAPE', 'android/tablet-10in-landscape'), ('IPAD LANDSCAPE', 'ios/ipad-landscape')]
for row, (label, folder) in enumerate(rows):
cd.text((32, 75 + row * 360), label, font=font(15, 'Bold', 'InstrumentSans'), fill='#CBA5FD')
for col, path in enumerate(sorted((OUT / folder).glob('*.png'))):