Init commit

This commit is contained in:
2022-01-26 00:25:07 +01:00
commit 0894692045
92 changed files with 2738 additions and 0 deletions

View File

@@ -0,0 +1,190 @@
import 'dart:math';
import 'package:tangocard_reader/models/yokutango_entry.dart';
import 'package:flutter/material.dart';
class Flashcard extends StatelessWidget {
final YokutangoEntry? card;
final int? cardIndex;
final bool isLeftSide;
final bool languageFlipped;
final int amountOfCardsOnStack;
final double rotationAmount;
const Flashcard({
this.card,
this.cardIndex,
this.isLeftSide = false,
this.languageFlipped = false,
this.amountOfCardsOnStack = 10,
this.rotationAmount = pi / 18,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final stackItems = <Widget>[];
final rng = Random();
for (int i = 0; i < amountOfCardsOnStack; i++) {
stackItems.add(
makeBlankCard(
offset: (isLeftSide ? -1 : 1) * (amountOfCardsOnStack - i) * 2,
rotationOrigin: isLeftSide ? 0.9 : 0.1,
angle: rng.nextDouble() * rotationAmount - rotationAmount / 2,
),
);
}
Widget? content;
if (card != null) {
if (isLeftSide && !languageFlipped || !isLeftSide && languageFlipped) {
content = _NorwegianContent(card: card!);
} else {
content = _JapaneseContent(card: card!);
}
}
stackItems.add(_FlashCardPaper(
child: content,
cardIndex: cardIndex,
));
return Center(
child: SizedBox(
width: MediaQuery.of(context).size.width / 2 - 60,
child: Stack(
children: stackItems,
),
),
);
}
Widget makeBlankCard({
required double offset,
required double rotationOrigin,
required double angle,
}) =>
Transform.translate(
offset: Offset(
offset,
0,
),
child: Transform.rotate(
angle: angle,
alignment: FractionalOffset(
rotationOrigin,
0.5,
),
child: const _FlashCardPaper()),
);
}
class _FlashCardPaper extends StatelessWidget {
final Widget? child;
final int? cardIndex;
const _FlashCardPaper({
this.child,
this.cardIndex,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
Widget? content;
if (child != null) {
content = Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(child: Container()),
child!,
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text((cardIndex != null) ? (cardIndex! + 1).toString() : "?")
],
),
),
],
);
}
return AspectRatio(
aspectRatio: 5 / 2,
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(),
),
child: content,
),
);
}
}
class _NorwegianContent extends StatelessWidget {
final YokutangoEntry card;
const _NorwegianContent({
required this.card,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: FittedBox(
fit: BoxFit.fitHeight,
child: DefaultTextStyle.merge(
style: const TextStyle(fontFamily: 'Heart Warming'),
child: Column(
children: card.norwegian.map((n) {
final text = (n.hints == null)
? n.word
: "${n.word} (${n.hints?.join(', ')})";
return Text(text);
}).toList(),
),
),
),
);
}
}
class _JapaneseContent extends StatelessWidget {
final YokutangoEntry card;
const _JapaneseContent({
required this.card,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
flex: 3,
child: FittedBox(
fit: BoxFit.fitHeight,
child: DefaultTextStyle.merge(
style: const TextStyle(fontFamily: 'K Gothic'),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: card.japanese
.map((e) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(e.word),
const Divider(),
Text(e.romaji ?? ""),
]))
.toList(),
),
),
),
);
}
}

22
lib/main.dart Normal file
View File

@@ -0,0 +1,22 @@
import 'package:tangocard_reader/router.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Tangocard Reader',
theme: ThemeData(
primarySwatch: Colors.blue,
),
initialRoute: '/',
onGenerateRoute: PageRouter.generateRoute,
);
}
}

View File

@@ -0,0 +1,8 @@
import 'package:tangocard_reader/models/yokutango_entry.dart';
class BenkyouArgs {
final List<YokutangoEntry> cards;
final int? index;
const BenkyouArgs({required this.cards, this.index});
}

View File

@@ -0,0 +1,72 @@
class YokutangoEntry {
final List<JapaneseWord> japanese;
final List<NorwegianWord> norwegian;
const YokutangoEntry({required this.japanese, required this.norwegian});
YokutangoEntry.fromJson(Map<String, dynamic> json)
: japanese = (json['japanese'] as List)
.map((e) => JapaneseWord.fromJson(e))
.toList(),
norwegian = (json['norwegian'] as List)
.map((e) => NorwegianWord.fromJson(e))
.toList();
@override
String toString() {
return "${japanese.join(" ### ")} - ${norwegian.join(" ### ")}";
}
YokutangoEntry.empty()
: japanese = [JapaneseWord.empty()],
norwegian = [NorwegianWord.empty()];
}
class JapaneseWord {
final String word;
final String? romaji;
final List<String>? hints;
const JapaneseWord({
required this.word,
this.romaji,
this.hints,
});
JapaneseWord.fromJson(Map<String, dynamic> json)
: word = json['word'],
romaji = json['romaji'],
hints = (json['hints'] as List?)?.map((e) => e as String).toList();
@override
String toString() {
return word +
((romaji != null) ? "/$romaji" : "") +
((hints != null) ? " (${hints!.join(", ")})" : "");
}
JapaneseWord.empty()
: word = "",
romaji = "",
hints = [];
}
class NorwegianWord {
final String word;
final List<String>? hints;
const NorwegianWord({required this.word, this.hints});
NorwegianWord.fromJson(Map<String, dynamic> json)
: word = json['word'],
hints = (json['hints'] as List?)?.map((e) => e as String).toList();
@override
String toString() {
return word + ((hints != null) ? " (${hints!.join(", ")})" : "");
}
NorwegianWord.empty()
: word = "",
hints = [];
}

35
lib/router.dart Normal file
View File

@@ -0,0 +1,35 @@
import 'dart:io';
import 'package:tangocard_reader/models/router_args.dart';
import 'package:tangocard_reader/screens/flashcard.dart';
import 'package:tangocard_reader/screens/home.dart';
import 'package:tangocard_reader/screens/tango_list.dart';
import 'package:flutter/material.dart';
class PageRouter {
static Route<dynamic> generateRoute(RouteSettings settings) {
final args = settings.arguments;
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const Home());
case '/tangolist':
final file = args as File;
return MaterialPageRoute(builder: (_) => TangoList(file: file));
case '/benkyou':
final benkyouArgs = args as BenkyouArgs;
return MaterialPageRoute(
builder: (_) => FlashcardView(
cards: benkyouArgs.cards,
index: benkyouArgs.index,
),
);
default:
return MaterialPageRoute(
builder: (_) => const Text("ERROR: this route does not exist"));
}
}
}

15
lib/screens/error.dart Normal file
View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class ErrorScreen extends StatelessWidget {
const ErrorScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(
child: Icon(
Icons.error,
color: Colors.red,
),
);
}
}

168
lib/screens/flashcard.dart Normal file
View File

@@ -0,0 +1,168 @@
import 'dart:math';
import 'package:tangocard_reader/components/flashcard.dart';
import 'package:tangocard_reader/models/yokutango_entry.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class FlashcardView extends StatefulWidget {
final List<YokutangoEntry> cards;
final int? index;
const FlashcardView({
required this.cards,
this.index,
Key? key,
}) : super(key: key);
@override
_FlashcardViewState createState() => _FlashcardViewState();
}
const encouragingWords = [
'頑張れ〜!',
'できるぞ!',
'ヨッシャー!',
'いけいけいけー!',
];
class _FlashcardViewState extends State<FlashcardView> {
String title = '';
int currentCard = 0;
final List<bool> _isSelected = [false, false];
get isShuffleMode => _isSelected[0];
get isLanguageSwitchedMode => _isSelected[1];
get randomCard => Random().nextInt(widget.cards.length);
@override
void initState() {
title = encouragingWords[Random().nextInt(encouragingWords.length)];
currentCard = widget.index ?? 0;
final isPhone = MediaQueryData.fromWindow(WidgetsBinding.instance!.window)
.size
.shortestSide <
600;
if (isPhone) {
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
}
super.initState();
}
@override
void dispose() {
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitDown,
DeviceOrientation.portraitUp,
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Row(
children: [
Expanded(child: Container()),
Text(title),
Expanded(child: Container()),
IconButton(
onPressed: () => setState(() {
currentCard = 0;
}),
icon: const Icon(Icons.repeat),
),
ToggleButtons(
children: const [
Icon(Icons.shuffle),
Icon(Icons.translate),
],
isSelected: _isSelected,
onPressed: (int index) {
setState(() {
_isSelected[index] = !_isSelected[index];
});
})
],
),
centerTitle: true,
),
body: _FlashcardPage(
card: widget.cards[currentCard],
index: currentCard,
languageFlipped: isLanguageSwitchedMode,
onNextCard: () {
setState(() {
currentCard = isShuffleMode ? randomCard : currentCard + 1;
if (currentCard == widget.cards.length) currentCard = 0;
title = encouragingWords[Random().nextInt(encouragingWords.length)];
});
},
),
);
}
}
class _FlashcardPage extends StatefulWidget {
final YokutangoEntry card;
final Function() onNextCard;
final bool languageFlipped;
final int? index;
const _FlashcardPage({
required this.card,
required this.onNextCard,
this.languageFlipped = false,
this.index,
Key? key,
}) : super(key: key);
@override
_FlashcardPageState createState() => _FlashcardPageState();
}
class _FlashcardPageState extends State<_FlashcardPage> {
bool isPressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (isPressed) {
widget.onNextCard();
}
setState(() {
isPressed = !isPressed;
});
},
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Flashcard(
card: widget.card,
cardIndex: widget.index,
isLeftSide: true,
languageFlipped: widget.languageFlipped,
),
const SizedBox(width: 40),
Flashcard(
card: isPressed ? widget.card : null,
cardIndex: widget.index,
languageFlipped: widget.languageFlipped,
),
],
),
),
);
}
}

81
lib/screens/home.dart Normal file
View File

@@ -0,0 +1,81 @@
import 'dart:convert';
import 'dart:io';
import 'package:tangocard_reader/models/yokutango_entry.dart';
import 'package:tangocard_reader/screens/error.dart';
import 'package:tangocard_reader/screens/loading.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("よく単語"),
centerTitle: true,
),
body: const TangocardList());
}
}
class TangocardList extends StatelessWidget {
const TangocardList({Key? key}) : super(key: key);
Future<Map<File, List<YokutangoEntry>>> get tangocardFilePaths => rootBundle
.loadString('AssetManifest.json')
.then(
(json) => jsonDecode(json)
.keys
.where((String key) =>
key.contains('yokutango/json/') && key.contains('.json'))
.toList(),
)
.then(
(l) async => Map.fromIterables(
l.map<File>((f) => File(f)),
await Future.wait<List<YokutangoEntry>>(
l
.map<Future<List<YokutangoEntry>>>(
(String t) => rootBundle
.loadString(t)
.then<List<YokutangoEntry>>((s) => jsonDecode(s)
.map<YokutangoEntry>(
(e) => YokutangoEntry.fromJson(e))
.toList()),
)
),
),
);
@override
Widget build(BuildContext context) {
return FutureBuilder(
future: tangocardFilePaths,
builder: (context, snapshot) {
if (snapshot.hasError) {
debugPrint(snapshot.error.toString());
return const ErrorScreen();
} else if (!snapshot.hasData) {
return const LoadingScreen();
}
return ListView(
children: (snapshot.data as Map<File, List<YokutangoEntry>>)
.entries
.map(
(e) => ListTile(
title: Text(
"${e.key.uri.pathSegments.last} - ${e.value.length} cards"),
onTap: () => Navigator.pushNamed(context, '/tangolist',
arguments: e.key),
),
)
.toList(),
);
},
);
}
}

12
lib/screens/loading.dart Normal file
View File

@@ -0,0 +1,12 @@
import 'package:flutter/material.dart';
class LoadingScreen extends StatelessWidget {
const LoadingScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return const Center(
child: CircularProgressIndicator(),
);
}
}

View File

@@ -0,0 +1,59 @@
import 'dart:convert';
import 'dart:io';
import 'package:tangocard_reader/models/router_args.dart';
import 'package:tangocard_reader/models/yokutango_entry.dart';
import 'package:tangocard_reader/screens/error.dart';
import 'package:tangocard_reader/screens/loading.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class TangoList extends StatelessWidget {
final File file;
const TangoList({
required this.file,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(file.uri.pathSegments.last),
),
body: FutureBuilder(
future:
rootBundle.loadString(file.path).then((data) => jsonDecode(data)),
builder: (context, jsonSnapshot) {
if (jsonSnapshot.hasError) {
return const ErrorScreen();
} else if (!jsonSnapshot.hasData) {
return const LoadingScreen();
}
final entries = (jsonSnapshot.data as List)
.map((e) => YokutangoEntry.fromJson(e))
.toList();
return ListView(
children: entries
.asMap()
.map(
(i, e) => MapEntry(
i,
ListTile(
title: Text(e.toString()),
onTap: () => Navigator.pushNamed(context, '/benkyou',
arguments: BenkyouArgs(cards: entries, index: i)),
),
),
)
.values
.toList(),
);
},
),
);
}
}