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
+8
View File
@@ -12,6 +12,11 @@ involved. The five screenshots follow the Features section of the website
4. Sharing: the Share sheet (people, groups, public link)
5. External storage: Files with the mounted-storage section
The landscape tablet sets show the app's real tablet layout (sidebar and
toolbar), captured from the actual shell widget. Portrait and landscape tablet
screenshots go in the same store slot; upload whichever orientation you want
to show, or both.
Look: the app's own design tokens (warm neutrals, one violet accent,
Schibsted Grotesk / Instrument Sans), flat shapes, alternating light and dark.
@@ -26,6 +31,9 @@ Schibsted Grotesk / Instrument Sans), flat shapes, alternating light and dark.
| Google Play 10-inch tablet screenshots | `android/tablet-10in/` | 1600 × 2560 |
| App Store iPhone screenshots (6.9" / 6.7" slot) | `ios/iphone/` | 1290 × 2796 |
| App Store iPad screenshots (13" slot) | `ios/ipad/` | 2064 × 2752 |
| Google Play 7-inch tablet, landscape | `android/tablet-7in-landscape/` | 1920 × 1200 |
| Google Play 10-inch tablet, landscape | `android/tablet-10in-landscape/` | 2560 × 1600 |
| App Store iPad screenshots, landscape (13" slot) | `ios/ipad-landscape/` | 2752 × 2064 |
| iOS icon reference | `ios/icon-1024.png` | 1024 × 1024 |
Upload in numeric order. The App Store uses the icon embedded in the uploaded
Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

After

Width:  |  Height:  |  Size: 455 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

+135
View File
@@ -224,6 +224,141 @@
"mode": "RGB",
"story": "external"
},
{
"file": "android/tablet-7in-landscape/01-accounts.png",
"size": [
1920,
1200
],
"mode": "RGB",
"story": "accounts"
},
{
"file": "android/tablet-7in-landscape/02-locks.png",
"size": [
1920,
1200
],
"mode": "RGB",
"story": "locks"
},
{
"file": "android/tablet-7in-landscape/03-sync.png",
"size": [
1920,
1200
],
"mode": "RGB",
"story": "sync"
},
{
"file": "android/tablet-7in-landscape/04-sharing.png",
"size": [
1920,
1200
],
"mode": "RGB",
"story": "sharing"
},
{
"file": "android/tablet-7in-landscape/05-external.png",
"size": [
1920,
1200
],
"mode": "RGB",
"story": "external"
},
{
"file": "android/tablet-10in-landscape/01-accounts.png",
"size": [
2560,
1600
],
"mode": "RGB",
"story": "accounts"
},
{
"file": "android/tablet-10in-landscape/02-locks.png",
"size": [
2560,
1600
],
"mode": "RGB",
"story": "locks"
},
{
"file": "android/tablet-10in-landscape/03-sync.png",
"size": [
2560,
1600
],
"mode": "RGB",
"story": "sync"
},
{
"file": "android/tablet-10in-landscape/04-sharing.png",
"size": [
2560,
1600
],
"mode": "RGB",
"story": "sharing"
},
{
"file": "android/tablet-10in-landscape/05-external.png",
"size": [
2560,
1600
],
"mode": "RGB",
"story": "external"
},
{
"file": "ios/ipad-landscape/01-accounts.png",
"size": [
2752,
2064
],
"mode": "RGB",
"story": "accounts"
},
{
"file": "ios/ipad-landscape/02-locks.png",
"size": [
2752,
2064
],
"mode": "RGB",
"story": "locks"
},
{
"file": "ios/ipad-landscape/03-sync.png",
"size": [
2752,
2064
],
"mode": "RGB",
"story": "sync"
},
{
"file": "ios/ipad-landscape/04-sharing.png",
"size": [
2752,
2064
],
"mode": "RGB",
"story": "sharing"
},
{
"file": "ios/ipad-landscape/05-external.png",
"size": [
2752,
2064
],
"mode": "RGB",
"story": "external"
},
{
"file": "android/icon-512.png",
"size": [
Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 320 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.
@@ -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'))):