Figma 설명 프레임 반영 — 로그인·온보딩·스플래시 · HOME · GALAXY · 퀘스트·이클립스·알림
This commit is contained in:
@@ -27,9 +27,18 @@ class FeatureFlags extends ChangeNotifier {
|
||||
/// STATION 탭 노출 스위치. 문서의 `stationEnabled`(bool) 하나를 읽는다.
|
||||
static const String stationKey = 'stationEnabled';
|
||||
|
||||
/// 운영 중인 최신 앱 버전(문자열 `x.y.z`). 스플래시 업데이트 확인 — Figma `510:19053`
|
||||
/// 「앱 버전 정보 · 최신 버전 여부 확인 · 업데이트 안내 노출」. 로그인 전에 읽어야 해서
|
||||
/// 같은 공개 문서에 둔다. 값이 없으면 안내를 띄우지 않는다.
|
||||
static const String latestVersionKey = 'latestAppVersion';
|
||||
|
||||
StreamSubscription<Map<String, Object?>>? _subscription;
|
||||
|
||||
bool _stationTab = false;
|
||||
String? _latestVersion;
|
||||
|
||||
/// 운영 최신 버전. 문서에 없거나 문자열이 아니면 null.
|
||||
String? get latestVersion => _latestVersion;
|
||||
|
||||
/// 하단 탭에 STATION 을 그릴 것인가. 기본 false.
|
||||
bool get stationTab => _stationTab;
|
||||
@@ -37,8 +46,13 @@ class FeatureFlags extends ChangeNotifier {
|
||||
void _apply(Map<String, Object?> data) {
|
||||
// 타입까지 확인한다. 누가 문자열 "true" 를 넣어도 켜지지 않는다.
|
||||
final next = data[stationKey] == true;
|
||||
if (next == _stationTab) return;
|
||||
final latest = switch (data[latestVersionKey]) {
|
||||
final String v when v.trim().isNotEmpty => v.trim(),
|
||||
_ => null,
|
||||
};
|
||||
if (next == _stationTab && latest == _latestVersion) return;
|
||||
_stationTab = next;
|
||||
_latestVersion = latest;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -54,3 +68,21 @@ class FeatureFlags extends ChangeNotifier {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// [installed] 가 [latest] 보다 낮은가 — 점으로 나눈 숫자를 앞에서부터 비교한다(`0.5.0` < `0.10.0`).
|
||||
/// 빌드 번호(`+7`)와 숫자가 아닌 꼬리는 무시한다. 읽을 수 없는 값이면 false(안내하지 않음).
|
||||
bool isOlderVersion(String installed, String latest) {
|
||||
List<int>? parse(String v) {
|
||||
final parts = v.split('+').first.split('.');
|
||||
final nums = [for (final p in parts) int.tryParse(p.trim())];
|
||||
return nums.any((n) => n == null) ? null : nums.cast<int>();
|
||||
}
|
||||
|
||||
final a = parse(installed), b = parse(latest);
|
||||
if (a == null || b == null) return false;
|
||||
for (var i = 0; i < a.length || i < b.length; i++) {
|
||||
final x = i < a.length ? a[i] : 0, y = i < b.length ? b[i] : 0;
|
||||
if (x != y) return x < y;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -263,12 +263,21 @@ class AppShell extends StatelessWidget {
|
||||
selectedIndex: selected < 0 ? 0 : selected,
|
||||
onDestinationSelected: (i) {
|
||||
// 다른 탭에서 HOME 으로 돌아오면 다시 읽는다(HOME 은 탭 전환으로 다시 만들어지지 않는다).
|
||||
if (branches[i] == 0 && shell.currentIndex != 0) homeRefreshSignal.value++;
|
||||
if (branches[i] == 0 && shell.currentIndex != 0)
|
||||
homeRefreshSignal.value++;
|
||||
final again = branches[i] == shell.currentIndex;
|
||||
shell.goBranch(
|
||||
branches[i],
|
||||
// initialLocation: true 를 주면 이미 열려 있는 탭을 다시 누를 때 루트로 되돌아간다.
|
||||
initialLocation: branches[i] == shell.currentIndex,
|
||||
initialLocation: again,
|
||||
);
|
||||
// 설명 프레임 `2097:16427` 「HOME 화면에서 클릭 시, Y스크롤값 초기화」(INSIDE · GALAXY ·
|
||||
// MY 동일) — 열려 있는 탭을 다시 누르면 그 탭 루트 화면을 맨 위로 올린다.
|
||||
if (again) {
|
||||
scrollTabToTop(
|
||||
shell.route.branches[branches[i]].navigatorKey,
|
||||
);
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
for (final tab in tabs)
|
||||
@@ -287,3 +296,35 @@ class AppShell extends StatelessWidget {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 탭 루트 화면의 세로 목록을 맨 위로. 루트 라우트의 [PrimaryScrollController] 를 쓴다 —
|
||||
/// 각 탭 화면이 따로 컨트롤러를 들고 있지 않아도 된다. 다시 누름으로 위에 쌓인 화면이 닫힌
|
||||
/// 다음 프레임에 움직인다.
|
||||
void scrollTabToTop(GlobalKey<NavigatorState> navigatorKey) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
ScrollController? found;
|
||||
void visit(Element e) {
|
||||
if (found != null) return;
|
||||
final w = e.widget;
|
||||
// 컨트롤러 없는 칸(`PrimaryScrollController.none`)과 붙은 목록이 없는 라우트는 건너뛴다.
|
||||
if (w is PrimaryScrollController && (w.controller?.hasClients ?? false)) {
|
||||
found = w.controller;
|
||||
return;
|
||||
}
|
||||
e.visitChildElements(visit);
|
||||
}
|
||||
|
||||
navigatorKey.currentContext?.visitChildElements(visit);
|
||||
for (final p in found?.positions.toList() ?? const <ScrollPosition>[]) {
|
||||
if (p.axis == Axis.vertical && p.pixels != p.minScrollExtent) {
|
||||
p.animateTo(
|
||||
p.minScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
// 탭이 이미 루트면 다시 그릴 일이 없어 위 콜백이 돌지 않는다 — 한 프레임을 청한다.
|
||||
WidgetsBinding.instance.ensureVisualUpdate();
|
||||
}
|
||||
|
||||
@@ -176,3 +176,17 @@ Future<void> showAlertDialog(
|
||||
confirmLabel: confirmLabel ?? context.l10n.commonConfirm,
|
||||
),
|
||||
);
|
||||
|
||||
/// 아래에서 올라오고 닫으면 아래로 내려가는 전체 화면 — Figma 설명 「닫기 버튼 클릭 시, 하단
|
||||
/// move out 페이지 종료」(`516:87382` 성운 생성 · `516:87457` 행성 생성 · `516:88164` 이클립스 상세).
|
||||
/// 안드로이드 기본 전환(확대)과 iOS 기본 전환(옆으로)이 모두 이 동작이 아니라 따로 둔다.
|
||||
Route<T> slideUpRoute<T>(WidgetBuilder builder) => PageRouteBuilder<T>(
|
||||
pageBuilder: (context, _, _) => builder(context),
|
||||
transitionsBuilder: (_, animation, _, child) => SlideTransition(
|
||||
position: Tween(
|
||||
begin: const Offset(0, 1),
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(curve: Curves.easeOutCubic)).animate(animation),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// 아래에서 올라와 아래로 닫히는 전체 화면 — Figma 설명 프레임의 「닫기 버튼 클릭 시, 하단 move out
|
||||
/// 페이지 종료 후 이전 화면 이동」(퀘스트 생성 `523:18123` · 위치 선택 `523:18207` · 신고하기 `548:21389`
|
||||
/// · 이클립스 상세 `516:88158`).
|
||||
///
|
||||
/// `MaterialPageRoute(fullscreenDialog: true)` 는 iOS 에서만 아래로 내려간다(Android 는 기본 전환).
|
||||
/// 두 플랫폼 모두 같은 방향으로 움직이게 전환만 바꾼다.
|
||||
class SheetRoute<T> extends MaterialPageRoute<T> {
|
||||
SheetRoute({required super.builder}) : super(fullscreenDialog: true);
|
||||
|
||||
@override
|
||||
Widget buildTransitions(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
Widget child,
|
||||
) => SlideTransition(
|
||||
position: animation.drive(
|
||||
Tween(begin: const Offset(0, 1), end: Offset.zero)
|
||||
.chain(CurveTween(curve: Curves.easeOutCubic)),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user