remusicalization作成ログ

RoadMap

  • アプリ作成 ✅ 2025-05-03
    • UI関連 ✅ 2025-05-03
      • main ✅ 2024-07-12
        • 遷移タブの作成 ✅ 2024-07-12
        • Appbarの作成 ✅ 2024-08-30
      • HomePage作成 ✅ 2025-05-03
        • UI作成 ✅ 2024-07-12
          • リスト表示 ✅ 2024-07-12
          • UIリフレッシュ ✅ 2024-11-20
        • 自動遷移 ✅ 2025-05-03
        • シャッフル再生 ✅ 2025-05-03
        • GDriveからのストリーミング ✅ 2025-05-03
        • Youtubeからのデータ取得 ✅ 2025-04-14
      • ListPage作成 ✅ 2025-05-03
        • UI作成 ✅ 2024-08-30
        • 自動遷移 ✅ 2025-05-03
        • シャッフル再生 ✅ 2025-05-03
        • List関連 ✅ 2025-04-14
          • Listの作成 ✅ 2024-09-06
          • Listの編集 ✅ 2025-04-14
          • LIstの削除 ✅ 2024-11-22
          • 作成されたListに新たに曲を追加 ✅ 2024-09-17
      • PlayPage作成 ✅ 2025-04-14
        • UI作成 ✅ 2024-08-09
          • 曲設定のDrawerの表示 ✅ 2024-07-16
          • イラストの表示 ✅ 2024-07-11
          • 秒数の表示 ✅ 2024-07-11
          • ボタンの追加 ✅ 2024-07-11
          • 音量調整ウィジェットの作成 ✅ 2024-07-11
          • シャッフルやループ切り替えボタンの作成 ✅ 2024-07-11
          • 歌詞UIの作成 ✅ 2024-08-09
        • 曲の再生 ✅ 2024-07-16
        • ループ ✅ 2024-07-16
        • 曲の移動 ✅ 2024-07-16
          • 次の曲に行く ✅ 2024-07-12
          • 前の曲に戻る ✅ 2024-07-12
          • シャッフル ✅ 2024-07-16
        • 設定画面 ✅ 2025-04-14
          • 自動音量調整機能 ✅ 2024-07-30
            • 編集UI作成 ✅ 2024-07-30
            • ロジック作成 ✅ 2024-07-30
          • 歌詞の入力 ✅ 2024-08-09
            • 編集UI作成 ✅ 2024-08-09
            • ロジック作成 ✅ 2024-08-09
          • ファイル名の変更 ✅ 2024-08-16
            • 編集UI作成 ✅ 2024-08-16
            • ロジック作成 ✅ 2024-08-16
              • ファイル名の変更 ✅ 2024-08-16
          • パッケージイラストの変更 ✅ 2024-08-11
            • 編集UI作成 ✅ 2024-08-11
            • ロジック作成 ✅ 2024-08-11
              • matrixGestureを使用 ✅ 2024-08-11
        • 曲が選択されていないときの処理 ✅ 2024-08-30
      • SettingPage作成 ✅ 2024-11-22
        • ライセンス表記 ✅ 2024-11-22
    • ロジック関連 ✅ 2024-07-05
      • Realm関連 ✅ 2024-07-05
        • モデルクラスの作成 ✅ 2024-07-05
        • 入出力処理の作成 ✅ 2024-07-05
        • 編集処理の作成 ✅ 2024-07-05
      • ファイル取得 ✅ 2024-07-05
      • 権限取得 ✅ 2024-07-05
        • androimanifestを編集する ✅ 2024-07-05

ログ

musicalizationを新たに作成しなおすログ
まずプロジェクトを作成する。

次にTabを実装する。
BottomNavigationBarウィジェット
main.dartに直接書くんじゃなくて代理ウィジェットを作成してそこに書いた。


これでmainが汚染される可能性は減ったでしょ

ダークモードの適用できた

theme: ThemeData.dark().copyWith(
	textTheme: ThemeData.dark().textTheme.copyWith(),
),

色の指定も作った

import 'package:flutter/material.dart';
 
enum MyColors{
	PRIMARY_BLUE
}
 
extension ColorEx on MyColors{
	Color get ARGB {
		switch(this){
			case MyColors.PRIMARY_BLUE: return const Color.fromARGB(255, 44, 232, 245);
		}
	}
}

ファイルへのアクセス権限要求をした。
_権限要求も同時に。

flutter pub add permission_handler
flutter pub add path_provider

これを参考にして色を変えていく

アプリ全体の色を変えてみた

theme: ThemeData.dark().copyWith(
	textTheme: ThemeData.dark().textTheme.copyWith(),
		colorScheme: ColorScheme.fromSeed(
		seedColor: MyColors.PRIMARY_BLUE.color,
	),
	iconTheme: IconThemeData(
		color: MyColors.PRIMARY_BLUE.color,
	),
	floatingActionButtonTheme: FloatingActionButtonThemeData(
		foregroundColor: MyColors.PRIMARY_BLUE.color,
	)
),

ここで色変更するのかそれぞれのウィジェットで色変更するのかどっちがいいんだろう
今回のアプリではここのmainの部分に書くことにしよう

AppBarウィジェットも作成

ヘッダーも作成

Widget buildCard(Size size, Widget widget, Function() callback){
	return Card(
	  child: InkWell(
		onTap: callback,
		child: SizedBox(
		  width: size.width * 0.3,
		  height: size.height * 0.09,
		  child: widget,
		),
	  ),
	);
}

これをInkCardとして定義

class InkCard extends StatelessWidget{
  final Function() onTap;
  final Widget child;
 
  const InkCard({super.key, required this.onTap, required this.child});
 
  @override
  Widget build(BuildContext context){
    final Size size = MediaQuery.of(context).size;
    return Card(
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(16)
      ),
      child: InkWell(
        borderRadius: MyBorderRadius.CARD.value,
        onTap: onTap,
        child: child
      ),
    );
  }
}

ファイル構成はこんな感じ

次はモデルクラスを作製しよう

Flutterでrealmを使用する

flutter pub add realm

Dialogを出すときに

import 'package:flutter/material.dart';
 
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

を宣言してからmain.dart

class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      debugShowCheckedModeBanner: false,
      theme: APP_THEME_DATA,
      navigatorKey: navigatorKey,
      home: const MyHomePage(),
    );
  }
}

を書いてから任意のファイルでnavigatorKeyを使えるようになるから

showWarnDialog(){
  showDialog(
    barrierDismissible: false,
    context: navigatorKey.currentContext!,
    builder: (BuildContext context){
      return 
    } 
  );
}

でどこでもdialogが出せるらしい

NavigatorKey

次にRealmの処理を完成させた
ジェネリクスを使ってめっちゃいい感じに作成できた

import 'package:musicalization/utils/Result.dart';
import 'package:musicalization/utils/showWarnDialog.dart';
import 'package:realm/realm.dart';
import 'realmInstanceFactory.dart';
 
class RealmIOManager {
  final realmInsFac = RealmInstanceFactory();
  final _reader = _DataReader();
  final _adder = _DataAdder();
  final _editor = _DataEditor();
  late final Realm realm;
 
  RealmIOManager(SchemaObject schemaObject) {
    realm = realmInsFac.createRealmInstance(schema: schemaObject);
  }
 
  Future<void> add<T extends RealmObject>({required T newData}) async {
    _adder.add<T>(realm: realm, newData: newData);
  }
 
  List<SCHEMA> readAll<SCHEMA extends RealmObject>() {
    Result results = _reader.readAll<SCHEMA>(realm: realm);
    return results.value;
  }
 
  SCHEMA searchById<SCHEMA extends RealmObject>({required ObjectId id}) {
    var result = _reader.searchById<SCHEMA>(realm: realm, id: id);
    return result.value;
  }
 
  Future<void> edit<T extends RealmObject>({required T newData}) async {
    _editor.edit<T>(realm: realm, newData: newData);
  }
 
  void delete<SCHEMA extends RealmObject>({required ObjectId id}) {
    var obj = searchById<SCHEMA>(id: id);
    realm.write(() => realm.delete<SCHEMA>(obj));
  }
 
  void deleteAll<SCHEMA extends RealmObject>() {
    realm.write(() => realm.deleteAll<SCHEMA>());
  }
}
 
class _DataReader {
  Result searchById<SCHEMA extends RealmObject>({required Realm realm, required ObjectId id}) {
    var infoResult = realm.find<SCHEMA>(id);
 
    late Result<SCHEMA> result;
 
    if (infoResult != null) {
      result = Result(
        isSucceeded: true,
        value: infoResult
      );
    } else {
      result = Result(
        isSucceeded: false,
        errorMsg: "That record is no Exists."
      );
      showWarnDialog(result.errorMsg);
    }
 
    return result;
  }
 
  Result<List<SCHEMA>> readAll<SCHEMA extends RealmObject>({required Realm realm}) {
    List<SCHEMA> infoResult = realm.all<SCHEMA>().toList();
 
    return Result(isSucceeded: true, value: infoResult);
  }
}
 
class _DataAdder {
  Future<Result> add<T extends RealmObject>({required Realm realm, required T newData}) async {
    realm.write(() => realm.add(newData));
    return Result(isSucceeded: true);
  }
}
 
class _DataEditor {
  Future<Result> edit<T extends RealmObject>({required Realm realm, required T newData}) async {
    realm.write(() => realm.add(newData, update: true));
    return Result(isSucceeded: true);
  }
} 
 

ちゃんとResultクラスも作成した
これめっちゃ便利なんだよな

class Result<T>{
  final bool isSucceeded;
  final T? value;
  final String errorMsg;
 
  Result({
    required this.isSucceeded,
    this.value,
    this.errorMsg = ""
  });
}

で音楽パッケージをインストール

flutter pub add audioplayers

ちょっと前までの音楽ファイルは保守性がめっちゃ低かったからさすがにリファクタする。
できた

今までで一番の出来のクラスになったわ

最初のデータベース処理を最適化しないと
まず、

	flowchart TB
	App[アプリ起動]
	
	App --> UI[UI描画]
	App --> FF[ファイルフェッチ]
	
	FF --> DB[データベース更新]
	
	FF --> LIST[リストの更新]
	
	UI --> LIST

こんな感じかな?
今は

flowchart TD

	APP --> StartUp
	StartUp --> Updater
	Updater --> FileFecher
	FileFecher --> |File|Updater
	
	Updater --> realmIOManager
	realmIOManager --> DataBase[(DB)]

こんな感じだけど
これを

flowchart TD
md[mainDeligater] --> su[StartUp]
md --> hp[Homepage]
hp --> hplb[ListBuilder]
su --> re[Repositry]
re --> ff[FileFetcher]
ff --> |files|re
re --> |files|mc[fileToMusic]
mc --> |musics|re
re --> |musics|ud[Updater]
re --> |musics|provider
provider --> hplb

にしたい

レコード更新処理だけどこれ主キーIDじゃなくてもいいな
でもObjectID使う方法で考えてみよう
originalListnewListのnameプロパティを比較して同じだったらcontinueする感じでもし違ったらcreateMusicクラスで作成してlistにaddかな

これで行けるな

if(oNamesList.contains(nMusic.name)) continue;
if(oNamesList.notContains(nMusic.name)) ...
if(nNamesList.notContains(oMusic.name))

これを何でイテレートするかだな

これはnListにあってoListにない曲をlistに追加する。

for(int i = 0; i < newList.length; i++){
  if(oNamesList.contains(newList[i].name)) continue;
  createdList.add(newList[i]);
}

これはoListにあってnListにない曲をlistから削除する。

for(int i = 0; i < originalList.length; i++){
  if(nNamesList.contains(originalList[i].name)) continue;
  createdList.remove(originalList[i]);
}

できた

次はこれをmainPageに適用させよう

ちょっと動画で見たけど
こんな感じのUIもいいかもしれない

こんな感じのUIにしたらいいけど、自分のは色が灰色じゃなくて黒だからこんな感じでは作成できないね

そんでもって音量調整機能の実装ができた
今回は結構設計をした
まず、

flowchart TD

PlayPage --> vb[volumeButton]
PlayPage --> vs[volumeSwitcher]
vs --> vcs[VolumeControlSlider]

vb --> |表示、非表示|pv1[provider]
pv2[provider] --> vs

こんな感じでproviderを使って兄弟要素をコントロールした
providerを使うとPageファイルが汚れなくていい

audioPlayerの問題を修正する。
なんか曲が終わった瞬間に実行されるリスナーが複数回実行される問題が発生したせいで曲が終わったあとに次の曲が再生はされるが、currentMusicプロパティが反映されないというよくわからん問題だった
ちなみにリスナーはこれ

class _PlayerCompletionListenerResistry {
  void setPlayerCompletionListener(
    AudioPlayer audioPlayer,
    MusicMode musicMode,
    Function() moveNext,
    Function() moveRandom,
    Function()? reRenderUI
  ) {
    audioPlayer.onPlayerComplete.listen((event) {
      switch(musicMode){
        case MusicMode.NORMAL: moveNext();
        case MusicMode.LOOP: () {};
        case MusicMode.SHUFFLE: moveRandom();
      }
 
      if(reRenderUI != null) reRenderUI();
    }); 
  }
}

これだとリスナーを登録したときの引数で実行されるされるからあんまり同期されない感じになってたらしい
これをクラス内に組み込んでこんな感じに修正した

void _initCompListener(){
	_audioPlayer.onPlayerComplete.listen((event) {
      switch(musicMode){
        case MusicMode.NORMAL: _moveNext();
        case MusicMode.LOOP: () {};
        case MusicMode.SHUFFLE: _moveRandom();
      }
 
      if(_reRenderUI != null) _reRenderUI();
    }); 
}

引数からインスタンス変数を参照するようにしてモード変更とかreRenderUIとかの関数オブジェクトに対して柔軟に対応できるように修正した
こうするとシングルトン内で同期がとれることに気づいた

でもなぜか三曲目に自動的に流れるようにすると勝手にindexが1にされるバグが発生する。
なぜか治ったこわい
治ってなかった
これ、

void setMusicList(List<Music> musicList, [String listName = ""]){
	_musicList = musicList;
	_listName = listName;
	_setIndex(musicList);
}

このメソッド、playPageUI描画時に呼び出されるんじゃなくてHomePageのScrollableList描画時に呼び出されてた
そのせいでplayhomeという遷移をするときにindexが初期化されちゃってたんだ
そんでindexが0になった状態で曲が流れて、次の曲はindex=1になるからこんなバグが起こってたんだ

これはこのメソッド実行前のvalueの値をバッファに保存することで対処した。

void _setIndex(List<Music> musicList){
	final value = _index.value;
	_index = Index(list: _musicList);
	_index.setIndex(value);
}

これにめっちゃ時間使っちゃった


これを使うとロック画面からいじれるらしい

publish: false

ドロワーの設定もする
Drawerウィジェット

前のmusiclizationからの引継ぎだけどとりあえず、ドロワーUIは作成できた
InkCardウィジェットを結構前に作成したんだけどこれがめっちゃ便利だからどっかにスニペットとして保存しておきたいかも


Settingにライセンス表記を追加した
ライセンス表記

publish: false

ドロワーの自動音量調整機能の追加をした
これは前のMusicalizationWithFlutterからコードを流用してリファクタリングしただけなので省略


Windows再インストールログでFlutterの環境がリセットされちゃったからここで実機デバッグ環境を整える

publish: false

歌詞の表示ウィジェットの作成をしてる
結構いい感じに設計できてる感じするわ

flowchart TD

pp[PlayPage] --> ls[LyricsSwitcher]
ls --> lf[LyricsFragment]

PlayPageからImage描画ウィジェットを拡張
これによってCreaterから選択し終わった後にどうやって描画するか問題が発生
これをProviderによって解決
問題点としてProviderはinitState内で値を変更できないこと
これは全てのウィジェットのビルドが終わったタイミングで処理を実行によって解決

  void _setMusicPictureToProv(){
    ref.read(musicImageProvider.notifier).update(
      (state) =>
        _player.currentMusic.picture != ""
        ? state
        : null
    );
  }
 
  @override
  Widget build(BuildContext context){
    final Size size = MediaQuery.of(context).size;
    final imageProv = ref.watch(musicImageProvider);
 
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _setMusicPictureToProv();
    });

WidgetsBindingの部分、Creater側ではボタンを押したらProviderに画像を格納する。
そのあと、このウィジェットが描画されたタイミングでProvderの値を変更して画像変更ウィジェットの完成

publish: false

Drawerを実装したページにてAppBarウィジェットに勝手にハンバーガーボタンが表示されてしまう問題は以下のコードで解決できる、

appBar: AppBar(
	automaticallyImplyLeading: false, // デフォルトで設置されるメニューボタンを表示させない
),

これyoutubeからダウンロードできるような仕組みも作成したい

ここにいろんな情報が載ってた
これらはどうやら動画そのものを取得することはできないっぽい?

pythonではやる方法があるっぽい

pyでのパッケージはpytubeというらしい
今度にこのパッケージを解析してやり方を学んでみようかな

publish: false

最初に起動した際にデータが読み込まれない問題が存在する。
データを最初にDBに保存する処理がどこかわからないのでそれを解析していく

どうやらStartUpRepoクラスにあるらしい
これを非同期で動作させて完成かと思ったけどなんか読み込みに時間がかかっている?

ちょっと調査してみたらなんかファイル読み込みの権限取得前にファイルを読み取ろうとしてたっぽい?
awaitを設定して完了した


プレイリストの作成
WrappedPlayList(以下、WPL)がPlayList(以下、PL)を継承して行う。
この際、WPLのmusicListにPL内のList<ObjectId>_ioManager<Music>を介してList<Music>に変換する
このときファクトリメソッド内にawaitを使用する必要がある。

class MyClass {
  final String data;
 
  MyClass._(this.data);
 
  static Future<MyClass> create() async {
    // 非同期処理を行う
    await Future.delayed(Duration(seconds: 2));
    return MyClass._('非同期データ');
  }
}
 
void main() async {
  print('データを取得中...');
  MyClass instance = await MyClass.create();
  print('取得したデータ: ${instance.data}');
}

list実装に伴い、playListクラスにpicture変数を用意

Flutterでrealmを使用する

PlayListとMusicを選択する画面の遷移にはRiverpodを用いてProviderを使用した。

その際に

class ReturnButtonFromMusicState extends ConsumerState<ReturnButtonFromMusic>{
  void returnPlayListPage(){
    ref.watch(isPlayListSelectedProvider.notifier).state = false;
  }
  
  @override
  Widget build(BuildContext context) {
    final prov = ref.watch(isPlayListSelectedProvider.notifier).state;
    return prov
    ? FloatingActionButton(
      backgroundColor: Theme.of(context).cardColor,
      onPressed: returnPlayListPage,
      child: const Icon(Icons.arrow_back),
    )
    : const SizedBox.shrink();
  }
}

このコードでは、provider変更後にUIが変わらないという問題が出た。
以下のコードに直すと変更が反映されるようになった。

final prov = ref.watch(isPlayListSelectedProvider);

ListViewの戻るボタンの調整とListのmusicListの編集機能を作成した。
この時なぜかhashcodeが同じMusicでもcontainsが反応しないっていうバグがあったが拡張関数で修正

extension MusicListContainsEx on List<Music> {
  bool containsAtMusic(Music music){
    for (Music ele in this) {
      if(ele.hashCode == music.hashCode) return true; 
    }
    return false;
  }
} 

ポップアップのコードを変更した
具体的にはポップアップにokとcancelのボタンを追加してユーザがどちらを押したかを呼び出し元へ帰す形にした
それによってよりこの関数が使いやすくなった
pop()メソッドの引数に値を入れるとどうやら呼び出し元に返却することができるらしい
詳細は以下のコードにて。

Future<Result> showWarnDialog(String text, {Function()? onOkTapped, Function()? onCancelTapped}) async {
  Widget buildOkButton(BuildContext context) {
    return onOkTapped != null
      ? TextButton(
          onPressed: () => Navigator.of(context).pop(Result(isSucceeded: true)),
          child: const Text("Ok"),
        )
      : const SizedBox.shrink();
  }
 
  Widget buildCancelButton(BuildContext context) {
    return onCancelTapped != null
      ? TextButton(
          onPressed: () => Navigator.of(context).pop(Result(isSucceeded: false)),
          child: const Text("Cancel"),
        )
      : const SizedBox.shrink();
  }
 
  final result = await showDialog<Result>(
    barrierDismissible: false,
    context: navigatorKey.currentContext!,
    builder: (BuildContext context) {
      return SimpleDialog(
        title: Center(
          child: PageWrapper(
            child: Column(
              children: [
                const Icon(
                  Icons.warning,
                  size: 75,
                  color: Colors.red,
                ),
                const StandardSpace(),
                Column(
                  children: [
                    FittedBox(
                      child: Text(text),
                    ),
                    const StandardSpace(),
                    Row(
                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
                      children: [
                        buildCancelButton(context),
                        buildOkButton(context),
                      ],
                    )
                  ]
                ),
                const StandardSpace(),
              ],
            )
          ),
        ),
      );
    },
  );
 
  return result ?? Result(isSucceeded: false);
}
 

リストを削除するときのUIの再レンダリング方法がわからない

publish: false

なぜか曲が再生できないバグがあった
解析した結果、setStateで変更した変数に依存しているウィジェットが再描画された際にinitStateが実行されないものによるものだった
今までできてた理由が正直わからないけど
FutureBuilderウィジェットによって解決することができた


音量調節ウィジェットが表示された時にした部分がoverflowしてしまっている問題を報告

あと動画をダウンロードしたときと消した時の処理がうまくいってないっぽいからそれも直す
Sliderウィジェット

publish: false

UIリフレッシュに関して反映されないバグあったけど修正した
Updaterクラスを以下のように編集

class Updater{
  final _io = RealmIOManager(Music.schema);
  final _fileFetcher = FileFetcher();
  final _creater = MusicCreater();
 
  Future<List<Music>> update() async {
    final List<Music> originalList = await _io.readAll<Music>();
    final List<Music> newList = await _fetchLocalStorage();
    final List<Music> createdList = _createUnDuplicateList(
      newList, 
      originalList 
    );
 
    final List<Music> listDiff = listDifference<Music>(originalList, createdList);
 
    await Future.wait(createdList.map((music) => _io.update<Music>(newData: music)));
    await Future.wait(listDiff.map((music) => _io.delete<Music>(id: music.id)));
    
    List<Music> musicList = await _io.readAll<Music>();
    return musicList;
  }
 
  Future<List<Music>> _fetchLocalStorage() async {
    final pathList = await _fileFetcher.pathList;
    final nameList = await _fileFetcher.nameList;
 
    final musicList = _creater.generateMusicList(pathList, nameList);
    return musicList;
  }
  
  List<Music> _createUnDuplicateList(List<Music> newList, List<Music> originalList){
    final List<String> nNamesList = newList.getNamesFromMusic();
    final List<String> oNamesList = originalList.getNamesFromMusic();
 
    List<Music> createdList = [...originalList];
 
    for(int i = 0; i < newList.length; i++){
      if(oNamesList.contains(newList[i].name)) continue;
      createdList.add(newList[i]);
    }
 
    for(int i = 0; i < originalList.length; i++){
      if(nNamesList.contains(originalList[i].name)) continue;
      createdList.remove(originalList[i]);
    }
 
    return createdList;
  }
}

リストの削除だけどFetcherインスタンスを関数が呼び出されるたびに生成するようにしたらばぐがなおった。