2023/04/03
Idea同士を線で結ぶための準備。(リレーション機能)
flutterの実装としてはRiverpodの利用にトライ。
Snackbarに、選択したIdeaと関連付けたIdeaの情報を表示。
flutterのソースファイル:main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'indent_line_painter.dart';
import 'dart:math';
import 'dart:convert'; //JSON形式のデータを扱う
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path_dart; //asが無いとcontextを上書きされてしまう
import 'dart:io';
import 'package:file_selector/file_selector.dart';
void main() => runApp( ProviderScope( child: const MyApp()) );
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: Scaffold(
body: SafeArea(child: CanvasZone()),
),
);
}
}
StateProvider<bool> _relatedIdeaSelectMode = StateProvider((ref) => false);
StateProvider<Idea?> _originIdea = StateProvider((ref) => null);
class CanvasZone extends StatefulWidget {
const CanvasZone({Key? key}) : super(key: key);
@override
State<CanvasZone> createState() => _CanvasZoneState();
}
class _CanvasZoneState extends State<CanvasZone> {
double _scale = 1.0;
List<Lane> lanes = [];
@override
void initState() {
super.initState();
addNewLane();
loadData();
WidgetsBinding.instance.addPostFrameCallback((_) {
lanes[0].ideas[0].key.currentState?._textFocusNode.requestFocus();
});
}
Future<void> addNewLaneAndFocusTitle(int targetLaneIndex) async {
if(targetLaneIndex == lanes.length) {
await addNewLane();
}
// ウィジェットが構築されるのを待つ
WidgetsBinding.instance.addPostFrameCallback((_) {
lanes[targetLaneIndex].key.currentState?._titleFocusNode.requestFocus();
});
}
Future<void> addNewLane() async {
setState(() {
lanes.add(Lane(
key: GlobalKey<_LaneState>(),
index: lanes.length,
ideas: [Idea(key: GlobalKey<_IdeaState>(), index: 0)]));
});
WidgetsBinding.instance.addPostFrameCallback((_) {
lanes[lanes.length-1].ideas[0].key.currentState?._textFocusNode.requestFocus();
});
}
void removeLane() {
setState(() {
if (lanes.length > 1) {
lanes.removeLast();
}
});
}
void focusNextLane(int targetLaneIndex, int targetIdeaIndex) {
if (targetLaneIndex < 0 || targetIdeaIndex < 0) return;
if (targetLaneIndex == lanes.length) {
addNewLane();
}
if (targetLaneIndex < lanes.length) {
for (int i = lanes[targetLaneIndex].ideas.length - 1;
i < targetIdeaIndex;
i++) {
if(lanes[targetLaneIndex].key.currentState == null) {
setState(() {
lanes[targetLaneIndex].ideas.add(Idea(key: GlobalKey<_IdeaState>(), index: i + 1));
for (int j = 0; j < lanes[targetLaneIndex].ideas.length; j++) {
lanes[targetLaneIndex].ideas[j].index = j;
}
});
} else {
lanes[targetLaneIndex].key.currentState?.addNewIdea(i);
}
}
lanes[targetLaneIndex]
.ideas[targetIdeaIndex]
.key
.currentState
?._textFocusNode
.requestFocus();
}
}
void scaleUp() {
setState(() {
_scale += 0.1;
});
}
void scaleDown() {
setState(() {
_scale = max(_scale - 0.1, 0.1);
});
}
Future<String> get _defaultFileName async {
final directory = await getApplicationDocumentsDirectory();
return '${directory.path}/default.json';
}
Future<void> loadData({String? fileName}) async {
fileName ??= await _defaultFileName;
final file = File(fileName);
if (await file.exists()==false) {
print('No such files:{$fileName}');
return;
}
String jsonString = await file.readAsString();
Map<String, dynamic> jsonMap = jsonDecode(jsonString);
List<Lane> loadedLanes = [];
for (var laneJson in jsonMap['lanes']) {
List<Idea> loadedIdeas = [];
for (var ideaJson in laneJson['ideas']) {
loadedIdeas.add(Idea(
key: GlobalKey<_IdeaState>(),
index: ideaJson['index'],
indentLevel: ideaJson['indentLevel'],
));
}
loadedLanes.add(Lane(
key: GlobalKey<_LaneState>(),
index: laneJson['index'] ?? 0,
title: laneJson['title'] ?? '',
ideas: loadedIdeas,
));
}
setState(() {
lanes = loadedLanes;
for (int i = 0; i < lanes.length; i++) {
for (int j = 0; j < lanes[i].ideas.length; j++) {
lanes[i].ideas[j].content =
jsonMap['lanes'][i]['ideas'][j]['content'];
}
}
});
}
Future<void> loadDataWithDialog() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'json',
extensions: ['json'],
);
final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]);
if (file != null) {
loadData(fileName: file.path);
}
}
Future<void> saveDataWithDialog() async {
String? path = await getSavePath(
acceptedTypeGroups: [
const XTypeGroup(label: 'json', extensions: ['json'])
],
suggestedName: "",
);
if (path != null) {
if ( path_dart.extension(path) == '' ) {
path = '$path.json';
}
saveData(fileName: path);
}
}
Future<void> saveData({String? fileName}) async {
List<Map<String, dynamic>> lanesJson = [];
fileName ??= await _defaultFileName;
for (var lane in lanes) {
List<Map<String, dynamic>> ideasJson = [];
for (var idea in lane.ideas) {
ideasJson.add({
'index': idea.index,
'indentLevel': idea.indentLevel,
'content': idea.key.currentState?._textController.text ?? '',
});
}
lanesJson.add({
'key': lane.key.toString(),
'index': lane.index,
'title': lane.key.currentState?._titleController.text ?? '',
'ideas': ideasJson,
});
}
String jsonString = jsonEncode({'lanes': lanesJson});
await File(fileName).writeAsString(jsonString);
}
@override
Widget build(BuildContext context) {
return CanvasZoneInheritedWidget(
canvasZoneState: this,
child: Column(
children: [
Align(
alignment: Alignment.topCenter,
child: Container(
margin: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: addNewLane,
child: const Text("レーン追加"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: removeLane,
child: const Text("レーン削除"),
),
const SizedBox(width: 30),
ElevatedButton(
onPressed: scaleUp,
child: const Text("拡大"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: scaleDown,
child: const Text("縮小"),
),
const SizedBox(width: 30),
ElevatedButton(
onPressed: saveData,
child: const Text("保存"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: loadData,
child: const Text("読込"),
),
const SizedBox(width: 30),
ElevatedButton(
onPressed: saveDataWithDialog,
child: const Text("名前を付けて保存"),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: loadDataWithDialog,
child: const Text("ファイルを読込"),
),
],
),
),
),
const Divider(),
Expanded(
child: Transform.scale(
scale: _scale,
alignment: Alignment.topLeft,
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: lanes.length,
itemBuilder: (context, index) {
return SizedBox(
width: MediaQuery.of(context).size.width * 0.4,
child: lanes[index],
);
},
),
),
),
],
),
);
}
}
class CanvasZoneInheritedWidget extends InheritedWidget {
final _CanvasZoneState canvasZoneState;
CanvasZoneInheritedWidget(
{required this.canvasZoneState, required Widget child})
: super(child: child);
@override
bool updateShouldNotify(CanvasZoneInheritedWidget oldWidget) => true;
static _CanvasZoneState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CanvasZoneInheritedWidget>()!
.canvasZoneState;
}
}
class Lane extends StatefulWidget {
@override
final GlobalKey<_LaneState> key;
int index;
String title;
List<Idea> ideas;
final double indentWidth=40; //インデントの下げ幅
Lane({
required this.key,
required this.index,
required this.ideas,
this.title = '',
}) : super(key: ValueKey('Lane$key'));
@override
State<Lane> createState() => _LaneState();
}
class _LaneState extends State<Lane> {
List<Idea> get ideas => widget.ideas;
final TextEditingController _titleController = TextEditingController();
final FocusNode _titleFocusNode = FocusNode();
void addNewIdea(int currentIndex) {
setState(() {
int currentIndentLevel =
ideas[currentIndex].indentLevel;
ideas.insert(
currentIndex + 1,
Idea(
key: GlobalKey<_IdeaState>(),
index: currentIndex + 1,
textFieldHeight: 48.0,
indentLevel: currentIndentLevel,
),
);
for (int i = currentIndex + 2; i < ideas.length; i++) {
ideas[i].index = i;
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
ideas[currentIndex+1].key.currentState?._textFocusNode.requestFocus();
//FocusScope.of(context).requestFocus(_textFocusNode);
});
}
void moveIdea(int currentIndex, int direction) {
if (currentIndex + direction >= 0 &&
currentIndex + direction < ideas.length) {
setState(() {
Idea temp = ideas[currentIndex];
ideas[currentIndex] = ideas[currentIndex + direction];
ideas[currentIndex + direction] = temp;
ideas[currentIndex].key.currentState?.updateIndex(currentIndex);
ideas[currentIndex + direction]
.key
.currentState
?.updateIndex(currentIndex + direction);
});
}
}
void deleteIdea(int currentIndex) {
if (ideas.length > 1) {
setState(() {
ideas.removeAt(currentIndex);
for (int i = currentIndex; i < ideas.length; i++) {
ideas[i].index = i;
}
if (currentIndex > 0) {
ideas[currentIndex - 1]
.key
.currentState
?._textFocusNode
.requestFocus();
} else {
ideas[currentIndex].key.currentState?._textFocusNode.requestFocus();
}
});
}
}
void focusAdjacentIdea(int currentIndex, int direction) {
if (currentIndex == 0 && direction <= 0) {
FocusScope.of(context).requestFocus(_titleFocusNode);
} else if (currentIndex + direction >= 0 &&
currentIndex + direction < ideas.length) {
ideas[currentIndex + direction]
.key
.currentState
?._textFocusNode
.requestFocus();
}
}
void redrawAffectedIdeas(int startIndex, int endIndex) {
if (startIndex < 0 || endIndex >= ideas.length) return;
setState(() {
for (int i = startIndex; i <= endIndex; i++) { //再描画を促す
//ideas[i].key.currentState?.indentLevel = widget.key.currentState?.indentLevel ?? indentLevel;
}
});
}
@override
void initState() {
super.initState();
_titleController.text = widget.title;
}
@override
Widget build(BuildContext context) {
return LaneInheritedWidget(
laneState: this,
child: Column(
children: [
Focus(
onKey: (FocusNode node, RawKeyEvent event) {
if (event.runtimeType == RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.arrowDown) { //カーソルキー ↓
ideas[0].key.currentState?._textFocusNode.requestFocus();
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight && //カーソルキー →
_titleController.selection.baseOffset ==
_titleController.text.length) {
CanvasZoneInheritedWidget
.of(context).addNewLaneAndFocusTitle(widget.index + 1);
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft && //カーソルキー ←
_titleController.selection.baseOffset == 0) {
if(widget.index > 0) {
CanvasZoneInheritedWidget
.of(context).lanes[widget.index-1].key.currentState?._titleFocusNode.requestFocus();
}
} }
return KeyEventResult.ignored;
},
child: Container(
color: Colors.blue[50],
padding: EdgeInsets.symmetric(horizontal: widget.indentWidth),
child: TextField(
controller: _titleController,
focusNode: _titleFocusNode,
decoration: InputDecoration(
border: InputBorder.none,
disabledBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.blue, width: 2.0),
),
// labelText: '',
fillColor: Colors.blue[50],
filled: true,
),
style: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.blue[900]),
keyboardType: TextInputType.text,
textInputAction: TextInputAction.done,
onSubmitted: (value) {
_titleFocusNode.unfocus();
},
),
)),
Expanded(
child: ListView.builder(
itemCount: ideas.length,
itemBuilder: (context, index) {
return ideas[index];
},
),
),
]
),
);
}
}
class LaneInheritedWidget extends InheritedWidget {
final _LaneState laneState;
LaneInheritedWidget({required this.laneState, required Widget child})
: super(child: child);
@override
bool updateShouldNotify(LaneInheritedWidget oldWidget) => true;
static _LaneState of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<LaneInheritedWidget>()!
.laneState;
}
}
class Idea extends ConsumerStatefulWidget {
@override
final GlobalKey<_IdeaState> key;
int index;
int indentLevel;
final double boxSpacing; //他のエディタとの間隔
final double textFieldHeight; //テキスト欄の高さ
String content;
List<Idea> relatedIdeas = [];
Idea({
required this.key,
required this.index,
this.boxSpacing = 8.0,
this.textFieldHeight = 48.0, //デフォルトは48
this.indentLevel = 0,
this.content = '',
}) : super(key: ValueKey('Idea$key'));
@override
_IdeaState createState() => _IdeaState();
}
class _IdeaState extends ConsumerState<Idea> with AutomaticKeepAliveClientMixin<Idea> {
final TextEditingController _textController = TextEditingController();
final FocusNode _textFocusNode = FocusNode();
void updateIndex(int newIndex) {
setState(() {
widget.index = newIndex;
});
}
void updateIndentLevel() {
setState(() {
});
}
// インデントの線を描画する
void _indentLinePaint(Canvas canvas, Size size) {
indentLinePaint(canvas, size, context, widget.index, widget.indentLevel,
widget.textFieldHeight, widget.boxSpacing, LaneInheritedWidget.of(context).widget.indentWidth);
}
String toJson() {
Map<String, dynamic> data = {
'index': widget.index,
'indentLevel': widget.indentLevel,
'text': _textController.text,
};
return json.encode(data);
}
@override
void initState() {
super.initState();
_textController.text = widget.content;
}
@override
bool get wantKeepAlive => true; //これが無いと画面外から出たときに中身も消えてしまう
@override
Widget build(BuildContext context) {
super.build(context);
return Focus(
onKey: (FocusNode node, RawKeyEvent event) {
if (event.runtimeType == RawKeyDownEvent) {
if (event.logicalKey == LogicalKeyboardKey.capsLock) {
// CapsLockキーを無視する条件を追加
return KeyEventResult.ignored;
} else if (event.isControlPressed) { //コントロールキー
bool needRepaint = false;
if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
LaneInheritedWidget.of(context).moveIdea(widget.index, -1);
needRepaint = true;
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
LaneInheritedWidget.of(context).moveIdea(widget.index, 1);
needRepaint = true;
}
if (needRepaint) {
findAndRepaintAffectedIdeas(context, widget.index, widget.indentLevel,
widget.index, widget.index + 1 /*Ctrlキーの場合、1つ下からチェックが必要*/);
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { //カーソルキー ↑
LaneInheritedWidget.of(context)
.focusAdjacentIdea(widget.index, -1);
} else if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
if (widget.index ==
LaneInheritedWidget.of(context).ideas.length - 1) {
LaneInheritedWidget.of(context).addNewIdea(widget.index);
} else {
LaneInheritedWidget.of(context)
.focusAdjacentIdea(widget.index, 1);
}
} else if (event.logicalKey == LogicalKeyboardKey.arrowRight && //カーソルキー →
_textController.selection.baseOffset ==
_textController.text.length) {
int laneIndex = CanvasZoneInheritedWidget.of(context)
.lanes
.indexOf(LaneInheritedWidget.of(context).widget);
CanvasZoneInheritedWidget.of(context)
.focusNextLane(laneIndex + 1, widget.index);
} else if (event.logicalKey == LogicalKeyboardKey.arrowLeft &&
_textController.selection.baseOffset == 0) {
int laneIndex = CanvasZoneInheritedWidget.of(context)
.lanes
.indexOf(LaneInheritedWidget.of(context).widget);
CanvasZoneInheritedWidget.of(context)
.focusNextLane(laneIndex - 1, widget.index);
} else if (event.logicalKey == LogicalKeyboardKey.tab) { //TABキー
if (event.isShiftPressed) {
setState(() {
widget.indentLevel = max(0, widget.indentLevel - 1);
});
} else {
setState(() {
widget.indentLevel += 1;
});
}
findAndRepaintAffectedIdeas(context, widget.index, widget.indentLevel,
widget.index, widget.index);
return KeyEventResult.handled; //デフォルトのタブの挙動を無効化して、フォーカスの移動を防ぐ
} else if ((event.logicalKey == LogicalKeyboardKey.delete ||
event.logicalKey == LogicalKeyboardKey.backspace) &&
_textController.text.isEmpty) {
LaneInheritedWidget.of(context).deleteIdea(widget.index);
} else if (event.logicalKey == LogicalKeyboardKey.f1) {
StateController<bool> mode = ref.watch(_relatedIdeaSelectMode.notifier);
mode.state = true;
ref.watch(_originIdea.notifier).state = this.widget;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('IdeaSelectMode:${mode.state} Lane:${CanvasZoneInheritedWidget.of(context).lanes.indexOf(LaneInheritedWidget.of(context).widget)} Idea:${widget.index}'),
),
);
}
}
return KeyEventResult.ignored;
},
child: RawKeyboardListener(
focusNode: FocusNode(),
child: Container(
margin: EdgeInsets.symmetric(
horizontal: 16, vertical: widget.boxSpacing),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(
width: 26,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red))),
Expanded(
flex: 4,
child: Row(children: [
CustomPaint(
painter: _CustomPainter(_indentLinePaint),
child: Container(width: LaneInheritedWidget.of(context).widget.indentWidth * widget.indentLevel),
),
Expanded(
child: SizedBox(
height: widget.textFieldHeight,
child: TextField(
controller: _textController,
focusNode: _textFocusNode,
maxLines: 1,
decoration: const InputDecoration(
border: OutlineInputBorder(),
),
keyboardType: TextInputType.multiline,
textInputAction: TextInputAction.done,
onSubmitted: (value) {
if ( ref.watch(_relatedIdeaSelectMode.notifier).state == true) {
StateController<bool> mode = ref.watch(_relatedIdeaSelectMode.notifier);
Idea? origin = ref.watch(_originIdea.notifier).state;
if( origin != null) {
this.widget.relatedIdeas.add( origin );
origin = null;
}
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('originIndex:${widget.relatedIdeas[0].index} - Lane:${CanvasZoneInheritedWidget.of(context).lanes.indexOf(LaneInheritedWidget.of(context).widget)} Idea:${widget.index}'),
),
);
mode.state = false;
} else {
_textFocusNode.unfocus();
LaneInheritedWidget.of(context)
.addNewIdea(widget.index);
}
},
),
),
),
]),
),
],
),
),
));
}
}
class _CustomPainter extends CustomPainter {
final void Function(Canvas, Size) _paint;
_CustomPainter(this._paint);
@override
void paint(Canvas canvas, Size size) {
_paint(canvas, size);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
indent_line_painter.dart
import 'package:flutter/material.dart';
import 'main.dart';
void indentLinePaint(Canvas canvas,Size size,BuildContext context, int index, int _indentLevel, double textFieldHeight, double boxSpacing, double indentWidth) {
// インデントの線を描画する
final paint = Paint()
..color = Colors.grey
..strokeWidth = 1;
var leftX = indentWidth / 2;
// 一行上のIdeaの_indentLevelを取得する
int upperIndentLevel = 0;
int upperIndex = index - 1;
//描画:Canvasの起点は左側センター
if (_indentLevel > 0 && index > 0 /*一番上の時はツリーを描画しない*/) {
//一番上の親が居ない場合の特別対応:自分の上のどこかに、indentLevelが小さい親がいるかチェック
bool cancelFlg = false; //描画しない特別な場合用
var i = index - 1;
while (i >= 0) {
if ((LaneInheritedWidget.of(context)
.ideas[i].indentLevel) <
_indentLevel) {
break;
}
i--;
}
if (i == -1) cancelFlg = true;
if (!cancelFlg) {
//縦の線:左側の基準
leftX = leftX + (indentWidth * (_indentLevel - 1));
//縦の線:自分の領域内で上に引く
double startY =
0 - (textFieldHeight / 2) - (boxSpacing * 2);
//縦の線:自分の領域外でどのくらい上まで引くかを計算
upperIndentLevel = LaneInheritedWidget.of(context)
.ideas[upperIndex].indentLevel;
if (_indentLevel <= upperIndentLevel) {
while (_indentLevel < upperIndentLevel && upperIndex >= 0) {
startY -= (textFieldHeight + boxSpacing * 2);
upperIndex--;
upperIndentLevel = LaneInheritedWidget.of(context)
.ideas[upperIndex].indentLevel;
}
if (_indentLevel <= upperIndentLevel) {
startY -= textFieldHeight / 2;
}
}
//縦の線
canvas.drawLine(Offset(leftX, 0), Offset(leftX, startY), paint);
//横の線
canvas.drawLine(Offset(leftX, size.height),
Offset(indentWidth * _indentLevel, size.height), paint);
}
}
}
void findAndRepaintAffectedIdeas(BuildContext context, int myIndex, int myIndentLevel, int startIndex, int endIndex){
// Find the start index
for (int i = myIndex - 1; i >= 0; i--) {
if (LaneInheritedWidget.of(context)
.ideas[i].indentLevel == myIndentLevel - 1) {
startIndex = i;
break;
}
}
// Find the end index
for (int i = myIndex + 1;
i < LaneInheritedWidget.of(context).ideas.length;
i++) {
if ((LaneInheritedWidget.of(context)
.ideas[i].indentLevel) >= myIndentLevel) {
endIndex = i;
break;
}
}
LaneInheritedWidget.of(context).redrawAffectedIdeas(startIndex, endIndex);
}
pubspec.yaml
name: in_lane
description: A new Flutter project.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=2.19.4 <3.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
path_provider: ^2.0.7
file_selector:
flutter_riverpod:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
2023/04/04
Idea同士を線で結ぶための準備。StackとCustomPaintを設置。Idea同士の関連付け情報のセーブとロードに対応(未完成)。
2023/04/05
firebaseでWebのホスティングをテスト。普通に動いた。
Windows用アプリを考えていたが、Webの方が便利かもしれない。
ただ文字入力などの動作が少し遅く感じる。
※本記事は、過去の記録(メモ)をもとに、他の方が読んで分かるように加筆・補足して掲載しています
テキストベースの思考整理ツール「アイディア・レーン」最新版はこちら。
コメント