-
Notifications
You must be signed in to change notification settings - Fork 1
Add Flutter port scaffold for SwitchBot Controller #35
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| .dart_tool/ | ||
| .packages | ||
| build/ | ||
| .pub/ | ||
| .idea/ | ||
| .vscode/ | ||
| *.iml |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| # SwitchBot Controller Flutter | ||
|
|
||
| Flutter 版の SwitchBot Controller です。既存の Electron 版と同じ機能を提供することを目的としています。 | ||
|
|
||
| ## セットアップ | ||
| ```bash | ||
| cd flutter_app | ||
| flutter pub get | ||
| flutter run | ||
| ``` | ||
|
|
||
| ## 機能 | ||
| - デバイス一覧表示・絞り込み・詳細のステータス表示 | ||
| - デバイスコマンドの送信 | ||
| - シーン一覧の取得と実行 | ||
| - API トークン/シークレットの保存と検証 | ||
| - ポーリング間隔、テーマ、言語の設定 | ||
| - 最後に開いていたタブの復元 |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,248 @@ | ||||||
| import 'dart:async'; | ||||||
|
|
||||||
| import 'package:flutter/material.dart'; | ||||||
| import 'package:shared_preferences/shared_preferences.dart'; | ||||||
|
|
||||||
| import 'models.dart'; | ||||||
| import 'services/switchbot_api.dart'; | ||||||
|
|
||||||
| class AppState extends ChangeNotifier { | ||||||
| AppState({required this.api}); | ||||||
|
|
||||||
| final SwitchBotApi api; | ||||||
|
|
||||||
| String _token = ''; | ||||||
| String _secret = ''; | ||||||
| int _pollingIntervalSeconds = 10; | ||||||
| ThemeMode _themeMode = ThemeMode.system; | ||||||
| AppLanguage _language = AppLanguage.japanese; | ||||||
| int _lastTabIndex = 0; | ||||||
|
|
||||||
| Timer? _pollingTimer; | ||||||
|
|
||||||
| List<Device> _devices = []; | ||||||
| List<Scene> _scenes = []; | ||||||
| bool _isLoading = false; | ||||||
| String? _errorMessage; | ||||||
|
|
||||||
| String get token => _token; | ||||||
| String get secret => _secret; | ||||||
| int get pollingIntervalSeconds => _pollingIntervalSeconds; | ||||||
| ThemeMode get themeMode => _themeMode; | ||||||
| AppLanguage get language => _language; | ||||||
| int get lastTabIndex => _lastTabIndex; | ||||||
|
|
||||||
| List<Device> get devices => _devices; | ||||||
| List<Scene> get scenes => _scenes; | ||||||
| bool get isLoading => _isLoading; | ||||||
| String? get errorMessage => _errorMessage; | ||||||
|
|
||||||
| Future<void> load() async { | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| _token = prefs.getString('token') ?? ''; | ||||||
| _secret = prefs.getString('secret') ?? ''; | ||||||
| _pollingIntervalSeconds = prefs.getInt('pollingIntervalSeconds') ?? 10; | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ポーリング間隔のデフォルト値
Suggested change
|
||||||
| _themeMode = ThemeMode.values[prefs.getInt('themeMode') ?? ThemeMode.system.index]; | ||||||
| _language = AppLanguage.values[prefs.getInt('language') ?? AppLanguage.japanese.index]; | ||||||
| _lastTabIndex = prefs.getInt('lastTabIndex') ?? 0; | ||||||
| notifyListeners(); | ||||||
|
|
||||||
| _restartPolling(); | ||||||
| } | ||||||
|
|
||||||
| Future<void> saveCredentials(String token, String secret) async { | ||||||
| _token = token; | ||||||
| _secret = secret; | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| await prefs.setString('token', token); | ||||||
| await prefs.setString('secret', secret); | ||||||
| notifyListeners(); | ||||||
| } | ||||||
|
|
||||||
| Future<void> clearCredentials() async { | ||||||
| _token = ''; | ||||||
| _secret = ''; | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| await prefs.remove('token'); | ||||||
| await prefs.remove('secret'); | ||||||
| notifyListeners(); | ||||||
| } | ||||||
|
|
||||||
| Future<void> setPollingIntervalSeconds(int value) async { | ||||||
| _pollingIntervalSeconds = value; | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| await prefs.setInt('pollingIntervalSeconds', value); | ||||||
| notifyListeners(); | ||||||
| _restartPolling(); | ||||||
| } | ||||||
|
|
||||||
| Future<void> setThemeMode(ThemeMode value) async { | ||||||
| _themeMode = value; | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| await prefs.setInt('themeMode', value.index); | ||||||
| notifyListeners(); | ||||||
| } | ||||||
|
|
||||||
| Future<void> setLanguage(AppLanguage value) async { | ||||||
| _language = value; | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| await prefs.setInt('language', value.index); | ||||||
| notifyListeners(); | ||||||
| } | ||||||
|
|
||||||
| Future<void> setLastTabIndex(int index) async { | ||||||
| _lastTabIndex = index; | ||||||
| final prefs = await SharedPreferences.getInstance(); | ||||||
| await prefs.setInt('lastTabIndex', index); | ||||||
| notifyListeners(); | ||||||
| } | ||||||
|
|
||||||
| Future<bool> validateCredentials() async { | ||||||
| if (_token.isEmpty || _secret.isEmpty) { | ||||||
| _errorMessage = '認証情報が未入力です。'; | ||||||
| notifyListeners(); | ||||||
| return false; | ||||||
|
Comment on lines
+100
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||
| } | ||||||
| try { | ||||||
| await api.fetchDevices(token: _token, secret: _secret); | ||||||
| _errorMessage = null; | ||||||
| notifyListeners(); | ||||||
| return true; | ||||||
| } catch (error) { | ||||||
| _errorMessage = error.toString(); | ||||||
| notifyListeners(); | ||||||
| return false; | ||||||
|
Comment on lines
+109
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
_errorMessage = null;
return true;
} catch (error) {
_errorMessage = error.toString();
return false;
} finally {
notifyListeners();
} |
||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Future<void> refreshDevices({bool showLoading = true}) async { | ||||||
| if (_token.isEmpty || _secret.isEmpty) { | ||||||
| _errorMessage = '認証情報が未入力です。'; | ||||||
| notifyListeners(); | ||||||
| return; | ||||||
| } | ||||||
| if (showLoading) { | ||||||
| _isLoading = true; | ||||||
| notifyListeners(); | ||||||
| } | ||||||
| try { | ||||||
| _devices = await api.fetchDevices(token: _token, secret: _secret); | ||||||
| _errorMessage = null; | ||||||
| } catch (error) { | ||||||
| _errorMessage = error.toString(); | ||||||
| } finally { | ||||||
| _isLoading = false; | ||||||
| notifyListeners(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Future<void> refreshScenes({bool showLoading = true}) async { | ||||||
| if (_token.isEmpty || _secret.isEmpty) { | ||||||
| _errorMessage = '認証情報が未入力です。'; | ||||||
| notifyListeners(); | ||||||
| return; | ||||||
| } | ||||||
| if (showLoading) { | ||||||
| _isLoading = true; | ||||||
| notifyListeners(); | ||||||
| } | ||||||
| try { | ||||||
| _scenes = await api.fetchScenes(token: _token, secret: _secret); | ||||||
| _errorMessage = null; | ||||||
| } catch (error) { | ||||||
| _errorMessage = error.toString(); | ||||||
| } finally { | ||||||
| _isLoading = false; | ||||||
| notifyListeners(); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Future<DeviceStatus?> fetchDeviceStatus(String deviceId) async { | ||||||
| if (_token.isEmpty || _secret.isEmpty) { | ||||||
| _errorMessage = '認証情報が未入力です。'; | ||||||
| notifyListeners(); | ||||||
| return null; | ||||||
| } | ||||||
| try { | ||||||
| final status = await api.fetchDeviceStatus( | ||||||
| token: _token, | ||||||
| secret: _secret, | ||||||
| deviceId: deviceId, | ||||||
| ); | ||||||
| _errorMessage = null; | ||||||
| notifyListeners(); | ||||||
| return status; | ||||||
| } catch (error) { | ||||||
| _errorMessage = error.toString(); | ||||||
| notifyListeners(); | ||||||
| return null; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Future<bool> executeScene(String sceneId) async { | ||||||
| if (_token.isEmpty || _secret.isEmpty) { | ||||||
| _errorMessage = '認証情報が未入力です。'; | ||||||
| notifyListeners(); | ||||||
| return false; | ||||||
| } | ||||||
| try { | ||||||
| await api.executeScene(token: _token, secret: _secret, sceneId: sceneId); | ||||||
| _errorMessage = null; | ||||||
| notifyListeners(); | ||||||
| return true; | ||||||
| } catch (error) { | ||||||
| _errorMessage = error.toString(); | ||||||
| notifyListeners(); | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| Future<bool> sendCommand( | ||||||
| String deviceId, { | ||||||
| required String command, | ||||||
| required String commandType, | ||||||
| String? parameter, | ||||||
| }) async { | ||||||
| if (_token.isEmpty || _secret.isEmpty) { | ||||||
| _errorMessage = '認証情報が未入力です。'; | ||||||
| notifyListeners(); | ||||||
| return false; | ||||||
| } | ||||||
| try { | ||||||
| await api.sendCommand( | ||||||
| token: _token, | ||||||
| secret: _secret, | ||||||
| deviceId: deviceId, | ||||||
| command: command, | ||||||
| commandType: commandType, | ||||||
| parameter: parameter, | ||||||
| ); | ||||||
| _errorMessage = null; | ||||||
| notifyListeners(); | ||||||
| return true; | ||||||
| } catch (error) { | ||||||
| _errorMessage = error.toString(); | ||||||
| notifyListeners(); | ||||||
| return false; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| void _restartPolling() { | ||||||
| _pollingTimer?.cancel(); | ||||||
| if (_pollingIntervalSeconds <= 0) { | ||||||
| return; | ||||||
| } | ||||||
| _pollingTimer = Timer.periodic( | ||||||
| Duration(seconds: _pollingIntervalSeconds), | ||||||
| (_) { | ||||||
| refreshDevices(showLoading: false); | ||||||
| }, | ||||||
| ); | ||||||
| } | ||||||
|
|
||||||
| @override | ||||||
| void dispose() { | ||||||
| _pollingTimer?.cancel(); | ||||||
| super.dispose(); | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import 'models.dart'; | ||
|
|
||
| class AppStrings { | ||
| AppStrings(this.language); | ||
|
|
||
| final AppLanguage language; | ||
|
|
||
| String get appTitle => _text('SwitchBot Controller', 'SwitchBot Controller'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| String get devices => _text('Devices', 'デバイス'); | ||
| String get scenes => _text('Scenes', 'シーン'); | ||
| String get settings => _text('Settings', '設定'); | ||
| String get tokenLabel => _text('Open Token', 'Open Token'); | ||
| String get secretLabel => _text('Secret Key', 'Secret Key'); | ||
| String get validateSave => _text('Validate and Save', '検証して保存'); | ||
| String get clearCredentials => _text('Clear Stored Credentials', '保存済み認証情報を削除'); | ||
| String get pollingInterval => _text('Polling Interval (sec)', 'ポーリング間隔(秒)'); | ||
| String get theme => _text('Theme', 'テーマ'); | ||
| String get language => _text('Language', '言語'); | ||
| String get refresh => _text('Refresh', '更新'); | ||
| String get filter => _text('Filter', '絞り込み'); | ||
| String get status => _text('Status', 'ステータス'); | ||
| String get execute => _text('Execute', '実行'); | ||
| String get command => _text('Command', 'コマンド'); | ||
| String get commandType => _text('Command Type', 'コマンド種別'); | ||
| String get parameter => _text('Parameter', 'パラメータ'); | ||
| String get sendCommand => _text('Send Command', 'コマンド送信'); | ||
| String get updateStatus => _text('Update Status', 'ステータス更新'); | ||
| String get noDevices => _text('No devices available.', 'デバイスがありません。'); | ||
| String get noScenes => _text('No scenes available.', 'シーンがありません。'); | ||
| String get validateSuccess => _text('Validation success.', '検証に成功しました。'); | ||
| String get validateFailure => _text('Validation failed.', '検証に失敗しました。'); | ||
| String get selectThemeSystem => _text('Follow System', 'システムに追従'); | ||
| String get selectThemeLight => _text('Light', 'ライト'); | ||
| String get selectThemeDark => _text('Dark', 'ダーク'); | ||
| String get selectLanguageJapanese => _text('Japanese', '日本語'); | ||
| String get selectLanguageEnglish => _text('English', 'English'); | ||
| String get lastUpdated => _text('Last Updated', '最終更新'); | ||
| String get apiError => _text('API error', 'API エラー'); | ||
|
|
||
| String _text(String en, String ja) { | ||
| return language == AppLanguage.english ? en : ja; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ポーリング間隔のデフォルト値
10がハードコードされています。この値をクラス定数として定義することで、コードの可読性と保守性が向上します。また、loadメソッドでのデフォルト値の取得もその定数を使用するように変更してください。