Skip to content
This repository was archived by the owner on Sep 12, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@
## 禁止事項
- .envファイルの内容をコミット・表示しないこと。
- 既存のテストを削除しないこと。
- こちらからの指示が無い場合はgitの操作は行わないこと。
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,8 @@
#### 失敗時のサウンド

- `~/.config/tree-image-optimizer/sounds/error` に、ユーザーが用意したサウンドファイルを配置すると、アプリケーションで選択可能になります。

### ユーザーが用意した背景画像を配置する

- ユーザー背景画像ファイルは、アプリケーションを再起動すると読み込まれます。
- `~/.config/tree-image-optimizer/wallpaper` に、ユーザーが用意した背景画像ファイルを配置すると、アプリケーションで選択可能になります。
Binary file added assets/images/wallpaper/wallpaper1.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/wallpaper/wallpaper2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed assets/images/wallpaper1.jpg
Binary file not shown.
Binary file removed assets/images/wallpaper2.jpg
Binary file not shown.
Binary file modified docs/design/feature/images/画面構成-設定.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 75 additions & 0 deletions docs/design/feature/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,78 @@
#### ℹ️
- ホバーするとpopoverに説明が表示される。
- 「出力先に既に同一のファイル名が存在する場合、上書きコピーされます。」というテキストが記述されていること。

### ✏️ 背景画像

#### 静的ラベル

- `背景画像` という静的ラベルを表示します。

##### プルダウンの選択肢の表示仕様

| 種別 | パス |
|----------------|----------------------------------------------|
| 同梱の背景画像 | `assets/images/wallpaper/wallpaper[1-3].jpg` |
| ユーザーが配置した背景画像 | `.config/tree-image-optimizer/wallpaper` |

- 上記をまとめてプルダウンに表示します。
- プルダウン選択肢の表示は特殊で、以下のように上部に同梱サウンドを、その後にユーザーサウンドを表示します。
```
同梱背景画像1.jpg
同梱背景画像2.jpg
ユーザー背景画像1.jpg
ユーザー背景画像2.png
```
- 同梱背景画像もユーザー背景画像もファイル名を照準でソートします。
- 選択肢の項目名は `ファイル名` を表示します。
- 同梱背景画像の場合だけ、 `(サンプル)xxx.jpg` という形式で、 `(サンプル)` をファイル名のprefixにしてください。
- ユーザー背景画像の場合は `ファイル名` を表示します。
- 対応している拡張子は以下です。
- `.jpg`
- `.png`
- ユーザー背景画像が `存在しない` または `権限不足で参照できない` 場合、 `同梱背景画像1` にフォールバックします。
- 背景画像はアスペクト比を保ったまま、アプリケーションの中に収まるようように設定してください。

#### ℹ️
- ホバーするとpopoverに説明が表示される。
- 以下のテキストが記述されていること。
```
.config/tree-image-optimizer/wallpaper 配下に背景画像ファイルを配置すると、自分が用意したファイルを選択することができます。
```

### ✏️ 背景画像の透明度

#### 静的ラベル

- `背景画像の透明度` という静的ラベルを表示します。

#### 背景画像の透明度スライダー

- 最小 = `0.0` 、 最大 = `1.0` です。
- 初期値は `1.0` です。
- この数値は `Opacity` の値です。

#### ℹ️
- ホバーするとpopoverに説明が表示される。
- 以下のテキストが記述されていること。
```
背景画像の透明度を調節することができます。
```

### ✏️ 背景色

#### 静的ラベル

- `背景色` という静的ラベルを表示します。

#### 背景色カラーピッカー

- カラーピッカーで背景色を設定することができます。
- 初期値は `白` です。

#### ℹ️
- ホバーするとpopoverに説明が表示される。
- 以下のテキストが記述されていること。
```
背景画像は縦横比を維持するため、余白できることがあります。その余白部分の色が背景色になります。
```
71 changes: 61 additions & 10 deletions lib/app/home_shell.dart
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:liquid_glass_easy/liquid_glass_easy.dart';
import 'package:material_ui/material_ui.dart';

import '../core/constants/app_constants.dart';
import 'providers.dart';
import '../ui/screens/about_screen.dart';
import '../ui/screens/convert_screen.dart';
import '../ui/screens/settings_screen.dart';
import '../ui/theme/glass_constants.dart';
import 'package:tree_image_optimizer/l10n/generated/app_localizations.dart';

/// 左サイドナビ・右コンテンツのレイアウトを持つ画面シェル。
class HomeShell extends StatefulWidget {
class HomeShell extends ConsumerStatefulWidget {
const HomeShell({super.key});

@override
State<HomeShell> createState() => _HomeShellState();
ConsumerState<HomeShell> createState() => _HomeShellState();
}

class _HomeShellState extends State<HomeShell> {
class _HomeShellState extends ConsumerState<HomeShell> {
int _selectedIndex = 0;

static const _screens = [ConvertScreen(), SettingsScreen(), AboutScreen()];
Expand All @@ -26,15 +30,21 @@ class _HomeShellState extends State<HomeShell> {
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final settings = ref.watch(settingsViewModelProvider);
final wallpaper = settings.wallpaper.isNotEmpty
? settings.wallpaper
: AppConstants.wallpaperAsset;
final wallpaperOpacity = settings.wallpaperOpacity;
final backgroundColor =
_parseColor(settings.wallpaperBackgroundColor) ??
const Color(0xFFFFFFFF);
return Scaffold(
body: LiquidGlassView(
backgroundWidget: Opacity(
opacity: AppConstants.wallpaperOpacity,
child: Image.asset(
AppConstants.wallpaperAsset,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
const ColoredBox(color: Color(0xFF1A1A2E)),
backgroundWidget: Container(
color: backgroundColor,
child: Opacity(
opacity: wallpaperOpacity,
child: _buildWallpaper(wallpaper),
),
),
realTimeCapture: false,
Expand Down Expand Up @@ -79,6 +89,47 @@ class _HomeShellState extends State<HomeShell> {
),
);
}

Widget _buildWallpaper(String source) {
// アスペクト比を保ったままアプリケーション内に収まるように contain を使用
if (source.startsWith('assets/')) {
return Image.asset(
source,
fit: BoxFit.contain,
alignment: Alignment.center,
errorBuilder: (context, error, stackTrace) =>
const ColoredBox(color: Color(0xFF1A1A2E)),
);
}
final file = File(source);
// 存在しない場合はフォールバックで同梱壁紙を表示
if (!file.existsSync()) {
return Image.asset(
AppConstants.wallpaperAsset,
fit: BoxFit.contain,
alignment: Alignment.center,
errorBuilder: (context, error, stackTrace) =>
const ColoredBox(color: Color(0xFF1A1A2E)),
);
}
return Image.file(
file,
fit: BoxFit.contain,
alignment: Alignment.center,
errorBuilder: (context, error, stackTrace) =>
const ColoredBox(color: Color(0xFF1A1A2E)),
);
}

Color? _parseColor(String hex) {
var h = hex.trim();
if (h.startsWith('#')) h = h.substring(1);
if (h.length == 6) h = 'FF$h';
if (h.length != 8) return null;
final v = int.tryParse(h, radix: 16);
if (v == null) return null;
return Color(v);
}
}

class _NavItem extends StatelessWidget {
Expand Down
9 changes: 9 additions & 0 deletions lib/app/providers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import '../data/services/path_service.dart';
import '../data/services/process_service.dart';
import '../data/services/sound_service.dart';
import '../data/services/tool_installer.dart';
import '../data/services/wallpaper_service.dart';
import '../data/services/upscale_service.dart';
import '../logic/usecases/compress_usecase.dart';
import '../logic/usecases/get_target_files_usecase.dart';
Expand Down Expand Up @@ -54,6 +55,13 @@ final soundServiceProvider = Provider<SoundService>((ref) {
);
});

final wallpaperServiceProvider = Provider<WallpaperService>((ref) {
return WallpaperService(
logService: ref.watch(logServiceProvider),
pathService: ref.watch(pathServiceProvider),
);
});

final notificationServiceProvider = Provider<NotificationService>((ref) {
return NotificationService(logService: ref.watch(logServiceProvider));
});
Expand Down Expand Up @@ -132,6 +140,7 @@ final settingsViewModelProvider =
return SettingsViewModel(
settingsRepository: ref.watch(settingsRepositoryProvider),
soundService: ref.watch(soundServiceProvider),
wallpaperService: ref.watch(wallpaperServiceProvider),
);
});

Expand Down
6 changes: 3 additions & 3 deletions lib/core/constants/app_constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ class AppConstants {

/// 壁紙アセットのパス。
///
/// `assets/images/` 配下の壁紙画像を指定する。変更時は `pubspec.yaml` の
/// `assets/images/wallpaper/` 配下の壁紙画像を指定する。変更時は `pubspec.yaml` の
/// `flutter.assets` も合わせて更新すること。
static const String wallpaperAsset = 'assets/images/wallpaper3.jpg';
static const String wallpaperAsset = 'assets/images/wallpaper/wallpaper1.jpg';

/// 壁紙の不透明度 (0.0: 透明 〜 1.0: 不透明)。
///
/// `HomeShell` の背景画像に適用される。値を下げると壁紙が薄くなり、
/// Liquid Glass の視認性が上がる。
static const double wallpaperOpacity = 1.0;
static const double wallpaperOpacity = 0.4;
}
43 changes: 43 additions & 0 deletions lib/core/models/settings_screen_settings.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:ui';

/// 設定画面の設定値。
class SettingsScreenSettings {
const SettingsScreenSettings({
Expand All @@ -6,6 +8,9 @@ class SettingsScreenSettings {
required this.successSound,
required this.errorSound,
this.language = '',
this.wallpaper = 'assets/images/wallpaper/wallpaper1.jpg',
this.wallpaperOpacity = 1.0,
this.wallpaperBackgroundColor = '#FFFFFF',
});

/// アプリ内で定めている初期値。
Expand All @@ -16,6 +21,9 @@ class SettingsScreenSettings {
successSound: 'assets/sounds/success/decision49.mp3',
errorSound: 'assets/sounds/error/beep1.mp3',
language: '',
wallpaper: 'assets/images/wallpaper/wallpaper1.jpg',
wallpaperOpacity: 1.0,
wallpaperBackgroundColor: '#FFFFFF',
);
}

Expand All @@ -34,19 +42,36 @@ class SettingsScreenSettings {
/// 選択された表示言語(BCP 47)。空文字は「環境設定に従う」を意味する。
final String language;

/// 背景画像のパス。
/// 同梱壁紙はアセットパス、ユーザー壁紙は絶対パス。
final String wallpaper;

/// 背景画像の透明度 (0.0〜1.0)。
final double wallpaperOpacity;

/// 背景色 (hex, 例: #FFFFFF)。
final String wallpaperBackgroundColor;

SettingsScreenSettings copyWith({
bool? showOsNotification,
bool? playSound,
String? successSound,
String? errorSound,
String? language,
String? wallpaper,
double? wallpaperOpacity,
String? wallpaperBackgroundColor,
}) {
return SettingsScreenSettings(
showOsNotification: showOsNotification ?? this.showOsNotification,
playSound: playSound ?? this.playSound,
successSound: successSound ?? this.successSound,
errorSound: errorSound ?? this.errorSound,
language: language ?? this.language,
wallpaper: wallpaper ?? this.wallpaper,
wallpaperOpacity: wallpaperOpacity ?? this.wallpaperOpacity,
wallpaperBackgroundColor:
wallpaperBackgroundColor ?? this.wallpaperBackgroundColor,
);
}

Expand All @@ -57,6 +82,24 @@ class SettingsScreenSettings {
'successSound': successSound,
'errorSound': errorSound,
'language': language,
'wallpaper': wallpaper,
'wallpaperOpacity': wallpaperOpacity,
'wallpaperBackgroundColor': wallpaperBackgroundColor,
};
}

/// 背景色を [Color] に変換する。不正な値の場合は白を返す。
Color get wallpaperBackgroundColorValue {
return _parseColor(wallpaperBackgroundColor) ?? const Color(0xFFFFFFFF);
}

static Color? _parseColor(String hex) {
var h = hex.trim();
if (h.startsWith('#')) h = h.substring(1);
if (h.length == 6) h = 'FF$h';
if (h.length != 8) return null;
final v = int.tryParse(h, radix: 16);
if (v == null) return null;
return Color(v);
}
}
22 changes: 22 additions & 0 deletions lib/core/models/wallpaper_option.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// 壁紙の選択肢を表す。
class WallpaperOption {
const WallpaperOption({
required this.name,
required this.source,
required this.isBundled,
});

/// 表示名(ファイル名)。
final String name;

/// ソース。
/// 同梱壁紙はアセットパス(`assets/images/wallpaper/...`)、ユーザー壁紙は絶対パス。
final String source;

/// 同梱壁紙かどうか。
final bool isBundled;

/// プルダウンに表示するラベル。
/// 同梱壁紙のみ `(サンプル)` をprefixに付ける。
String get label => isBundled ? '(サンプル)$name' : name;
}
Loading