W NoticeBand 2: 나머지 안내 띠 다섯 곳을 공용 띠로 (관리자 10/24 ① - 모양을 하나로)
- 옮긴 곳: 실시간 동기화 카드 (멈춤 · 마스터 기다림) · AI 그림 화면 안내 2 · AI 서비스 창 경고 · FTP 평문 경고 · 동영상 오류 상자
- NoticeBand 더한 것: 설명은 없어도 됨 · selectable (오류 원문을 골라 복사 - 설명이 없으면 제목) · margin (부르는 쪽이 정함)
- 글 · 단추 · 색의 뜻은 그대로 (경고 = warning · 오류 = danger/alert · 안내 = accent). 새 화면 글 없음
- 범위 밖: Tor 상태 줄 · 브라우저 띠 (진행 막대가 든 상태 줄 - 안내 띠가 아님)
- 시험 기대값 바꿈 1곳: sync_problem_test 의 '대상 파일은 지우지 않았습니다.' 를 따로 된 글이 아니라 띠 설명 안의 글로 찾음
(textContaining - 그 문장이 보인다는 뜻은 그대로)
캡처 (바꾸기 전 판 dac47bd 2회 · 바꾼 판 2회, 전후 18장: M:\jj_tmp\dev\7f\nb_compare):
- 바뀐 것은 옮긴 화면 18장뿐 (AI 그림 4 · FTP 평문 6 · 새 캡처 notice_places 8), 다른 화면은 그대로. 412 폭 넘침 없음
- 새 캡처 8장은 바뀐 판 두 번 0px. AI 그림 넓은 2장 · FTP 평문 960 2장은 바꾸기 전에도 흔들리던 장 (글 칸 커서)
사용자가 보게 될 동작: 안내 띠가 모두 같은 모양 (연한 바탕 · 테두리 · 아이콘 · 굵은 제목 · 작은 설명 · 단추). FTP 평문 경고는 빨간 글 상자에서 빨간 띠로
확인: analyze 0 · 단위 1525 ×2 · 통합 6/6 ×2 (b · c) · base10 214장 0px
(nb3 통합 a 에서 412 지우기 "시험 휴지통 폴더가 없어짐" 이 다시 남 - 이 고침과 무관, 이제 step_check 가 늘 %TEMP% 감시를 켬)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
modular
parent
dac47bdd20
commit
9b03f20d8f
|
|
@ -18,6 +18,7 @@ import 'app_actions.dart';
|
|||
import 'browser_page.dart' show browserRouteObserver;
|
||||
import 'reader_page.dart';
|
||||
import 'theme.dart';
|
||||
import 'notice_band.dart';
|
||||
|
||||
/// 123: AI 그림 - 글 → 그림 · 그림 → 그림. 기기 안 (SD1.5 + LCM) 이 기본이고, 사용자가 추가한 서버 · 서비스도 같은 화면에서.
|
||||
/// 결과는 새 파일로 저장하고 (원본을 덮지 않음), 옆에 프롬프트 · 시드 · 모델을 json 으로 남긴다.
|
||||
|
|
@ -423,19 +424,6 @@ class _AiImagePageState extends State<AiImagePage> with RouteAware {
|
|||
),
|
||||
]);
|
||||
|
||||
Widget _notice(IconData icon, Color color, String text, {Widget? action}) => Container(
|
||||
margin: const EdgeInsets.only(top: 8),
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Row(children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(text, style: const TextStyle(fontSize: 13))),
|
||||
?action,
|
||||
]),
|
||||
);
|
||||
Widget _notice(IconData icon, Color color, String text, {Widget? action}) =>
|
||||
NoticeBand(icon: icon, color: color, title: text, margin: const EdgeInsets.only(top: 8), actions: [?action]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import 'license_texts.dart' show showLicenseDocument;
|
|||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import '../app/ai_upscale.dart' show deviceMemTotalMb, upscaleMemoryLines;
|
||||
import 'notice_band.dart';
|
||||
|
||||
/// 기기 안에서 그리는 데 아직 받지 않은 파일 (Windows 는 실행 파일까지, TAESD 포함) - AI 그림 화면 · 받기 화면이 같은 값을 보인다 (139)
|
||||
List<AiFile> aiMissingFiles(AiStore store) => [
|
||||
|
|
@ -485,11 +486,8 @@ Future<void> editAiService(BuildContext context, AppController c, {AiService? ol
|
|||
content: SizedBox(
|
||||
width: 480,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: JjColors.warning.withValues(alpha: 0.1),
|
||||
child: Text(tr('이 서비스로 그리면 그림 · 프롬프트가 기기 밖 (이 주소) 으로 나갑니다.'), style: const TextStyle(fontSize: 12)),
|
||||
),
|
||||
NoticeBand(
|
||||
icon: Icons.public, color: JjColors.warning, margin: EdgeInsets.zero, title: tr('이 서비스로 그리면 그림 · 프롬프트가 기기 밖 (이 주소) 으로 나갑니다.')),
|
||||
TextField(controller: name, decoration: InputDecoration(labelText: tr('이름'))),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: kind,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import 'confirm.dart';
|
|||
import 'setting_tile.dart';
|
||||
import 'theme.dart';
|
||||
import 'webdav_settings.dart' show davServerUsers;
|
||||
import 'notice_band.dart';
|
||||
|
||||
/// l: 환경 설정 > 파일 탐색기 > FTP · FTPS: 서버 목록 (추가 · 고치기 · 지우기 · 연결 확인). SFTP 와 같은 틀.
|
||||
/// 서버마다 파일 탐색기 · Rsync 화면 위쪽 (WebDAV 옆) 에 탭이 생긴다. 평문 FTP 는 목록 · 탭 · 창에 "암호화 안 됨" 을 늘 보인다
|
||||
|
|
@ -243,13 +244,7 @@ Future<FtpServer?> editFtpServer(BuildContext context, AppController c, {FtpServ
|
|||
]),
|
||||
),
|
||||
if (security == 'none')
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
color: JjColors.danger.withValues(alpha: 0.12),
|
||||
child: Text(
|
||||
tr('⚠ 평문 FTP 는 아이디 · 비밀번호 · 파일이 암호화되지 않은 채 오갑니다. 같은 망의 다른 사람이 볼 수 있습니다. 서버가 FTPS 를 못 할 때만, 믿을 수 있는 집 안 망에서만 쓰세요.'),
|
||||
style: TextStyle(fontSize: 12, color: JjColors.danger)),
|
||||
),
|
||||
NoticeBand(icon: Icons.lock_open, color: JjColors.danger, margin: EdgeInsets.zero, title: tr('⚠ 평문 FTP 는 아이디 · 비밀번호 · 파일이 암호화되지 않은 채 오갑니다. 같은 망의 다른 사람이 볼 수 있습니다. 서버가 FTPS 를 못 할 때만, 믿을 수 있는 집 안 망에서만 쓰세요.')),
|
||||
TextField(controller: user, decoration: InputDecoration(labelText: tr('아이디 (비우면 익명)'))),
|
||||
TextField(
|
||||
controller: pass,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import '../theme.dart';
|
|||
import '../../l10n/tr.dart';
|
||||
import 'move_to_target.dart';
|
||||
import 'subtitle_row.dart';
|
||||
import '../notice_band.dart';
|
||||
|
||||
class VideoDetailPane extends StatelessWidget {
|
||||
final AppController c;
|
||||
|
|
@ -179,15 +180,7 @@ class VideoDetailPane extends StatelessWidget {
|
|||
],
|
||||
if (v.message != null) ...[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: JjColors.danger.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: SelectableText(v.message!,
|
||||
style: TextStyle(color: JjColors.danger, fontSize: 12)),
|
||||
),
|
||||
NoticeBand(icon: Icons.error_outline, color: JjColors.danger, margin: EdgeInsets.zero, title: v.message!, selectable: true),
|
||||
],
|
||||
if (!compact) ...[
|
||||
const SizedBox(height: 24),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import '../sync_preview_view.dart' show unbreakableOption;
|
|||
import '../theme.dart';
|
||||
import 'live_sync_flow.dart';
|
||||
import 'transfer_widgets.dart';
|
||||
import '../notice_band.dart';
|
||||
|
||||
/// 원본 문제 안내 (68 · 70): 원본을 읽지 못함 · 원본이 빔 · 마스터 비밀번호 기다림
|
||||
class LiveSourceProblemPanel extends StatelessWidget {
|
||||
|
|
@ -43,27 +44,13 @@ class LiveSourceProblemPanel extends StatelessWidget {
|
|||
final running = live.isRunning(x);
|
||||
// 128: 마스터 비밀번호를 기다리는 중 - 고장이 아니라 넣으면 이어서 맞춘다
|
||||
if (e.locked) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: JjColors.accent.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: JjColors.accent.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(Icons.lock_clock_outlined, color: JjColors.accent, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(tr('마스터 비밀번호가 필요해 기다리는 중'),
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.accent)),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(tr('이 서버의 저장된 비밀번호를 쓰려면 마스터 비밀번호가 필요합니다. 넣으면 이어서 맞춥니다. 대상 파일은 건드리지 않았습니다.'),
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
const SizedBox(height: 6),
|
||||
return NoticeBand(
|
||||
icon: Icons.lock_clock_outlined,
|
||||
color: JjColors.accent,
|
||||
margin: EdgeInsets.zero,
|
||||
title: tr('마스터 비밀번호가 필요해 기다리는 중'),
|
||||
body: tr('이 서버의 저장된 비밀번호를 쓰려면 마스터 비밀번호가 필요합니다. 넣으면 이어서 맞춥니다. 대상 파일은 건드리지 않았습니다.'),
|
||||
actions: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: running
|
||||
? null
|
||||
|
|
@ -73,54 +60,33 @@ class LiveSourceProblemPanel extends StatelessWidget {
|
|||
icon: const Icon(Icons.lock_open, size: 18),
|
||||
label: Text(tr('마스터 비밀번호 넣기')),
|
||||
),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 8),
|
||||
decoration: BoxDecoration(
|
||||
color: JjColors.alert.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: JjColors.alert.withValues(alpha: 0.6)),
|
||||
),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Icon(e.empty ? Icons.folder_off_outlined : Icons.error_outline, color: JjColors.alert, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
e.empty ? tr('원본 폴더가 비어 있어 멈췄습니다') : tr('원본을 읽을 수 없어 멈췄습니다'),
|
||||
style: TextStyle(fontWeight: FontWeight.w600, color: JjColors.alert),
|
||||
),
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(tr('대상 파일은 지우지 않았습니다.'), style: const TextStyle(fontSize: 12)),
|
||||
Text(
|
||||
e.empty
|
||||
? trf('원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. '
|
||||
'일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.', [dels.length])
|
||||
: tr('SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.'),
|
||||
style: const TextStyle(fontSize: 12),
|
||||
return NoticeBand(
|
||||
icon: e.empty ? Icons.folder_off_outlined : Icons.error_outline,
|
||||
margin: EdgeInsets.zero,
|
||||
title: e.empty ? tr('원본 폴더가 비어 있어 멈췄습니다') : tr('원본을 읽을 수 없어 멈췄습니다'),
|
||||
body: [
|
||||
tr('대상 파일은 지우지 않았습니다.'),
|
||||
e.empty
|
||||
? trf('원본에는 아무것도 없는데 대상에는 {0}개가 있습니다. SD 카드가 빠졌거나 네트워크 · 권한 문제일 수 있습니다. '
|
||||
'일부러 원본을 비웠다면 지울 목록을 보고 [그래도 맞추기] 를 누르세요.', [dels.length])
|
||||
: tr('SD 카드가 꽂혀 있는지, 네트워크 (NAS · VPN) 가 연결되어 있는지, 폴더 권한이 있는지 확인한 뒤 [다시 시도] 를 누르세요.'),
|
||||
'${vDisplay(x.source)}${e.detail.isEmpty ? '' : ' · ${e.detail}'}',
|
||||
].join('\n'),
|
||||
actions: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: running ? null : () => live.syncNow(x),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(tr('다시 시도')),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('${vDisplay(x.source)}${e.detail.isEmpty ? '' : ' · ${e.detail}'}',
|
||||
style: TextStyle(fontSize: 11, color: JjColors.textDim)),
|
||||
const SizedBox(height: 6),
|
||||
Wrap(spacing: 8, runSpacing: 4, children: [
|
||||
FilledButton.tonalIcon(
|
||||
onPressed: running ? null : () => live.syncNow(x),
|
||||
icon: const Icon(Icons.refresh, size: 18),
|
||||
label: Text(tr('다시 시도')),
|
||||
if (e.empty && dels.isNotEmpty)
|
||||
OutlinedButton(
|
||||
onPressed: running ? null : () => confirmEmptySourceSync(context, live, x),
|
||||
child: Text(tr('지울 목록 보고 결정')),
|
||||
),
|
||||
if (e.empty && dels.isNotEmpty)
|
||||
OutlinedButton(
|
||||
onPressed: running ? null : () => confirmEmptySourceSync(context, live, x),
|
||||
child: Text(tr('지울 목록 보고 결정')),
|
||||
),
|
||||
]),
|
||||
]),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
/// 약속 (W 공용 위젯, 설계 7-2 NoticeBand): 목록 · 화면 위에 띄우는 안내 띠 하나.
|
||||
/// 입력: 아이콘 · 제목 · 설명 · 단추들 · 색 (처음 값 경고색). 출력: 연한 바탕 · 테두리 · 둥근 띠 - 제목은 굵게, 설명은 작고 흐리게,
|
||||
/// 입력: 아이콘 · 제목 · 설명 (없어도 됨, [selectable] 이면 골라 복사) · 단추들 · 색 (처음 값 경고색) · 바깥 여백 ([margin]). 출력: 연한 바탕 · 테두리 · 둥근 띠 - 제목은 굵게, 설명은 작고 흐리게,
|
||||
/// 단추는 넘치면 다음 줄로 (Wrap). 오류 없음. 폭 · 글자 배율 · 언어에 따라 제목 · 설명은 줄을 바꾼다 (넘치지 않음).
|
||||
/// 쓰는 곳: 파일 탐색기 안내 (권한 · 읽지 못함 · 만들다 만 파일 · 시스템 폴더 · 루트 · 서버 열쇠). 화면은 부르기만 한다
|
||||
/// 쓰는 곳: 파일 탐색기 안내 (권한 · 읽지 못함 · 만들다 만 파일 · 시스템 폴더 · 루트 · 서버 열쇠) · 실시간 동기화 멈춤 · 마스터 기다림 ·
|
||||
/// AI 그림 화면 · AI 서비스 · FTP 평문 경고 · 동영상 오류. 화면은 부르기만 한다
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
|
@ -9,11 +10,23 @@ import 'package:flutter/material.dart';
|
|||
import 'theme.dart';
|
||||
|
||||
class NoticeBand extends StatelessWidget {
|
||||
const NoticeBand({super.key, required this.icon, required this.title, required this.body, this.actions = const [], this.color});
|
||||
const NoticeBand(
|
||||
{super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.body,
|
||||
this.actions = const [],
|
||||
this.color,
|
||||
this.selectable = false,
|
||||
this.margin = const EdgeInsets.fromLTRB(6, 6, 6, 0)});
|
||||
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String body;
|
||||
final String? body;
|
||||
|
||||
/// 설명 (설명이 없으면 제목) 을 골라 복사할 수 있게 (오류 원문)
|
||||
final bool selectable;
|
||||
final EdgeInsetsGeometry margin;
|
||||
final List<Widget> actions;
|
||||
|
||||
/// 띠 색 (처음 값 [JjColors.alert])
|
||||
|
|
@ -24,7 +37,7 @@ class NoticeBand extends StatelessWidget {
|
|||
final c = color ?? JjColors.alert;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.fromLTRB(6, 6, 6, 0),
|
||||
margin: margin,
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 6),
|
||||
decoration: BoxDecoration(
|
||||
color: c.withValues(alpha: 0.12),
|
||||
|
|
@ -35,10 +48,17 @@ class NoticeBand extends StatelessWidget {
|
|||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Icon(icon, size: 18, color: c),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: Text(title, style: const TextStyle(fontWeight: FontWeight.w600))),
|
||||
Expanded(
|
||||
child: selectable && body == null
|
||||
? SelectableText(title, style: const TextStyle(fontWeight: FontWeight.w600))
|
||||
: Text(title, style: const TextStyle(fontWeight: FontWeight.w600))),
|
||||
]),
|
||||
const SizedBox(height: 4),
|
||||
Text(body, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
if (body case final b?) ...[
|
||||
const SizedBox(height: 4),
|
||||
selectable
|
||||
? SelectableText(b, style: TextStyle(fontSize: 12, color: JjColors.textDim))
|
||||
: Text(b, style: TextStyle(fontSize: 12, color: JjColors.textDim)),
|
||||
],
|
||||
Wrap(spacing: 8, children: actions),
|
||||
]),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -139,7 +139,8 @@ void main() {
|
|||
await tester.tap(find.text('lsync (실시간 동기화)'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('원본 폴더가 비어 있어 멈췄습니다'), findsOneWidget);
|
||||
expect(find.text('대상 파일은 지우지 않았습니다.'), findsOneWidget);
|
||||
// W NoticeBand: 설명 줄들이 띠의 설명 한 덩어리로 (글은 그대로 보임)
|
||||
expect(find.textContaining('대상 파일은 지우지 않았습니다.'), findsOneWidget);
|
||||
expect(find.text('맞출 것 없음 (원본과 대상이 같습니다)'), findsNothing);
|
||||
expect(find.text('다시 시도'), findsOneWidget);
|
||||
await tester.tap(find.text('지울 목록 보고 결정'));
|
||||
|
|
|
|||
Loading…
Reference in New Issue