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>
@@ -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
|
||||
|
||||
|
After Width: | Height: | Size: 220 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 290 KiB |
|
After Width: | Height: | Size: 238 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 239 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 306 KiB After Width: | Height: | Size: 455 KiB |
|
After Width: | Height: | Size: 249 KiB |
|
After Width: | Height: | Size: 241 KiB |
|
After Width: | Height: | Size: 385 KiB |
|
After Width: | Height: | Size: 318 KiB |
|
After Width: | Height: | Size: 268 KiB |
@@ -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": [
|
||||
|
||||
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 147 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 149 KiB |
|
After Width: | Height: | Size: 252 KiB |
|
After Width: | Height: | Size: 255 KiB |
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 268 KiB |
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 320 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 147 KiB |
@@ -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();
|
||||
|
||||
@@ -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'))):
|
||||
|
||||