synonyms;
7 |
8 | const WordView({
9 | super.key,
10 | required this.word,
11 | required this.meanings,
12 | required this.synonyms,
13 | });
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return NestedScrollView(
18 | headerSliverBuilder: (context, innerBoxIsScrolled) {
19 | return [
20 | SliverAppBar(
21 | expandedHeight: 100,
22 | floating: false,
23 | pinned: true,
24 | leading: const SizedBox(),
25 | flexibleSpace: FlexibleSpaceBar(
26 | centerTitle: false,
27 | title: Text(
28 | word.title ?? "",
29 | style: TextStyles.headingStyle2.bold
30 | .size(25)
31 | .textColor(Colors.white),
32 | ),
33 | ),
34 | ),
35 | ];
36 | },
37 | body: Container(
38 | constraints: const BoxConstraints.expand(),
39 | decoration: const BoxDecoration(
40 | gradient: LinearGradient(
41 | begin: Alignment.topLeft,
42 | end: Alignment.bottomRight,
43 | colors: [Colors.white, ThemeColors.accent, ThemeColors.primary],
44 | ),
45 | ),
46 | child: SingleChildScrollView(
47 | padding: const EdgeInsets.all(10),
48 | child: Column(
49 | crossAxisAlignment: CrossAxisAlignment.start,
50 | children: [
51 | if (word.conjugation?.isNotEmpty ?? false)
52 | Html(
53 | data: "Mnyambuliko: ${word.conjugation}
",
54 | style: {"p": Style(fontSize: FontSize(20))},
55 | ),
56 | WordDetails(
57 | word: word,
58 | meanings: meanings,
59 | synonyms: synonyms,
60 | ),
61 | ],
62 | ),
63 | ),
64 | ),
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/presentation/screens/home/widgets/lists/idioms_list.dart:
--------------------------------------------------------------------------------
1 | part of '../../home_screen.dart';
2 |
3 | class IdiomsList extends StatelessWidget {
4 | final HomeScreenState parent;
5 | const IdiomsList({super.key, required this.parent});
6 |
7 | @override
8 | Widget build(BuildContext context) {
9 | final Size size = MediaQuery.of(context).size;
10 |
11 | var emptyState = const EmptyState(title: 'Samahani hamna chochote hapa');
12 |
13 | var listView = ListView.builder(
14 | padding: const EdgeInsets.only(right: 15),
15 | itemCount: parent.filteredIdioms.length,
16 | itemBuilder: (BuildContext context, int index) {
17 | final Idiom idiom = parent.filteredIdioms[index];
18 | return IdiomItem(idiom: idiom);
19 | },
20 | );
21 |
22 | return SizedBox(
23 | width: size.width - 85,
24 | child: parent.filteredIdioms.isEmpty ? emptyState : listView,
25 | );
26 | }
27 | }
28 |
29 | class IdiomItem extends StatelessWidget {
30 | final Idiom idiom;
31 | const IdiomItem({super.key, required this.idiom});
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | final titleTxtStyle =
36 | TextStyles.headingStyle4.bold.size(22).textHeight(1.2);
37 | final bodyTxtStyle = TextStyles.bodyStyle1.size(18).textHeight(2);
38 |
39 | var meaning = cleanMeaning(idiom.meaning ?? "");
40 |
41 | final contents = meaning.split("|");
42 | var extra = contents.isNotEmpty ? contents[0].split(":") : [];
43 | meaning = extra.isNotEmpty ? " ~ ${extra[0].trim()}." : "";
44 |
45 | if (contents.length > 1) {
46 | extra = contents[1].split(":");
47 | meaning = "$meaning\n ~ ${extra[0].trim()}.";
48 | }
49 |
50 | return Card(
51 | elevation: 2,
52 | child: ListTile(
53 | title: Text(idiom.title ?? "", style: titleTxtStyle),
54 | subtitle: Column(
55 | crossAxisAlignment: CrossAxisAlignment.start,
56 | children: [
57 | if (meaning.isNotEmpty)
58 | Text(meaning, style: bodyTxtStyle, maxLines: 2),
59 | const SizedBox(height: 10),
60 | ],
61 | ),
62 | ),
63 | );
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | id "dev.flutter.flutter-gradle-plugin"
5 | }
6 |
7 | def keystoreProperties = new Properties()
8 | def keystorePropertiesFile = rootProject.file('keystore/key.properties')
9 | if (keystorePropertiesFile.exists()) {
10 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
11 | }
12 |
13 | android {
14 | namespace = "com.swahilib"
15 | compileSdk = 34
16 | ndkVersion = flutter.ndkVersion
17 |
18 | compileOptions {
19 | sourceCompatibility = JavaVersion.VERSION_1_8
20 | targetCompatibility = JavaVersion.VERSION_1_8
21 | }
22 |
23 | kotlinOptions {
24 | jvmTarget = JavaVersion.VERSION_1_8
25 | }
26 |
27 | defaultConfig {
28 | applicationId "com.swahilib"
29 | minSdkVersion 24
30 | targetSdkVersion 34
31 | compileSdkVersion 34
32 | multiDexEnabled true
33 | versionCode 125
34 | versionName "1.0.125"
35 | }
36 |
37 | signingConfigs {
38 | release {
39 | keyAlias keystoreProperties['keyAlias']
40 | keyPassword keystoreProperties['keyPassword']
41 | storePassword keystoreProperties['storePassword']
42 | storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
43 | }
44 | }
45 |
46 | flavorDimensions "flavors"
47 | productFlavors {
48 | production {
49 | dimension "flavors"
50 | applicationIdSuffix ""
51 | manifestPlaceholders = [appName: "Swahilib"]
52 | }
53 | develop {
54 | dimension "flavors"
55 | applicationIdSuffix ".dev"
56 | manifestPlaceholders = [appName: "Dev Swahilib"]
57 | }
58 | }
59 |
60 | buildTypes {
61 | debug {
62 | signingConfig signingConfigs.debug
63 | }
64 | release {
65 | minifyEnabled true
66 | signingConfig signingConfigs.release
67 | proguardFiles getDefaultProguardFile('proguard-android.txt')
68 | }
69 | }
70 | }
71 |
72 | flutter {
73 | source = "../.."
74 | }
75 |
--------------------------------------------------------------------------------
/lib/presentation/widgets/progress/skeleton.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:skeleton_loader/skeleton_loader.dart';
3 |
4 | import '../../theme/theme_colors.dart';
5 | import '../../theme/theme_styles.dart';
6 |
7 | class SkeletonLoading extends StatelessWidget {
8 | const SkeletonLoading({super.key});
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | final size = MediaQuery.of(context).size;
13 | var rowWidget = Container(
14 | margin:
15 | const EdgeInsets.only(left: Sizes.m, right: Sizes.m, top: Sizes.m),
16 | child: Column(
17 | mainAxisAlignment: MainAxisAlignment.start,
18 | crossAxisAlignment: CrossAxisAlignment.start,
19 | children: [
20 | Container(
21 | margin: const EdgeInsets.only(bottom: Sizes.xs),
22 | height: 15,
23 | width: size.width - 120,
24 | color: Colors.black,
25 | ),
26 | Container(
27 | margin: const EdgeInsets.only(bottom: Sizes.xs),
28 | height: 30,
29 | width: size.width - 50,
30 | color: Colors.black,
31 | ),
32 | Row(
33 | children: [
34 | Container(
35 | height: 10,
36 | width: size.width / 6,
37 | color: Colors.black,
38 | ),
39 | SizedBox(width: Sizes.xs),
40 | Container(
41 | height: 10,
42 | width: size.width / 6,
43 | color: Colors.black,
44 | ),
45 | SizedBox(width: Sizes.xs),
46 | Container(
47 | height: 10,
48 | width: size.width / 5,
49 | color: Colors.black,
50 | ),
51 | ],
52 | ),
53 | ],
54 | ));
55 |
56 | return SingleChildScrollView(
57 | child: SkeletonLoader(
58 | builder: rowWidget,
59 | items: 10,
60 | period: const Duration(seconds: 5),
61 | highlightColor: ThemeColors.primary,
62 | direction: SkeletonDirection.ltr,
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/presentation/screens/home/widgets/lists/sayings_list.dart:
--------------------------------------------------------------------------------
1 | part of '../../home_screen.dart';
2 |
3 | class SayingsList extends StatelessWidget {
4 | final HomeScreenState parent;
5 | const SayingsList({super.key, required this.parent});
6 |
7 | @override
8 | Widget build(BuildContext context) {
9 | final Size size = MediaQuery.of(context).size;
10 |
11 | var emptyState = const EmptyState(title: 'Samahani hamna chochote hapa');
12 |
13 | var listView = ListView.builder(
14 | padding: const EdgeInsets.only(right: 15),
15 | itemCount: parent.filteredSayings.length,
16 | itemBuilder: (BuildContext context, int index) {
17 | final Saying saying = parent.filteredSayings[index];
18 | return SayingItem(saying: saying);
19 | },
20 | );
21 |
22 | return SizedBox(
23 | width: size.width - 85,
24 | child: parent.filteredSayings.isEmpty ? emptyState : listView,
25 | );
26 | }
27 | }
28 |
29 | class SayingItem extends StatelessWidget {
30 | final Saying saying;
31 | const SayingItem({super.key, required this.saying});
32 |
33 | @override
34 | Widget build(BuildContext context) {
35 | final titleTxtStyle = TextStyles.headingStyle4.bold
36 | .size(22)
37 | .textHeight(1.2);
38 | final bodyTxtStyle = TextStyles.bodyStyle1.size(18);
39 |
40 | var meaning = cleanMeaning(saying.meaning ?? "");
41 |
42 | final contents = meaning.split("|");
43 | var extra = contents.isNotEmpty ? contents[0].split(":") : [];
44 | meaning = extra.isNotEmpty ? " ~ ${extra[0].trim()}." : "";
45 |
46 | if (contents.length > 1) {
47 | extra = contents[1].split(":");
48 | meaning = "$meaning\n ~ ${extra[0].trim()}.";
49 | }
50 |
51 | return Card(
52 | elevation: 2,
53 | child: ListTile(
54 | onTap: () {}, //=> vm.openSaying(saying),
55 | title: Text(saying.title ?? "", style: titleTxtStyle),
56 | subtitle: Column(
57 | crossAxisAlignment: CrossAxisAlignment.start,
58 | children: [
59 | if (meaning.isNotEmpty)
60 | Text(meaning, style: bodyTxtStyle, maxLines: 2),
61 | const SizedBox(height: 10),
62 | ],
63 | ),
64 | ),
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/windows/runner/flutter_window.cpp:
--------------------------------------------------------------------------------
1 | #include "flutter_window.h"
2 |
3 | #include
4 |
5 | #include "flutter/generated_plugin_registrant.h"
6 |
7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project)
8 | : project_(project) {}
9 |
10 | FlutterWindow::~FlutterWindow() {}
11 |
12 | bool FlutterWindow::OnCreate() {
13 | if (!Win32Window::OnCreate()) {
14 | return false;
15 | }
16 |
17 | RECT frame = GetClientArea();
18 |
19 | // The size here must match the window dimensions to avoid unnecessary surface
20 | // creation / destruction in the startup path.
21 | flutter_controller_ = std::make_unique(
22 | frame.right - frame.left, frame.bottom - frame.top, project_);
23 | // Ensure that basic setup of the controller was successful.
24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) {
25 | return false;
26 | }
27 | RegisterPlugins(flutter_controller_->engine());
28 | SetChildContent(flutter_controller_->view()->GetNativeWindow());
29 |
30 | flutter_controller_->engine()->SetNextFrameCallback([&]() {
31 | this->Show();
32 | });
33 |
34 | // Flutter can complete the first frame before the "show window" callback is
35 | // registered. The following call ensures a frame is pending to ensure the
36 | // window is shown. It is a no-op if the first frame hasn't completed yet.
37 | flutter_controller_->ForceRedraw();
38 |
39 | return true;
40 | }
41 |
42 | void FlutterWindow::OnDestroy() {
43 | if (flutter_controller_) {
44 | flutter_controller_ = nullptr;
45 | }
46 |
47 | Win32Window::OnDestroy();
48 | }
49 |
50 | LRESULT
51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message,
52 | WPARAM const wparam,
53 | LPARAM const lparam) noexcept {
54 | // Give Flutter, including plugins, an opportunity to handle window messages.
55 | if (flutter_controller_) {
56 | std::optional result =
57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam,
58 | lparam);
59 | if (result) {
60 | return *result;
61 | }
62 | }
63 |
64 | switch (message) {
65 | case WM_FONTCHANGE:
66 | flutter_controller_->engine()->ReloadSystemFonts();
67 | break;
68 | }
69 |
70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam);
71 | }
72 |
--------------------------------------------------------------------------------
/lib/domain/data_init_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:supabase_flutter/supabase_flutter.dart';
2 |
3 | import '../data/models/models.dart';
4 | import '../core/utils/app_util.dart';
5 |
6 | class DataInitRepository {
7 | final SupabaseClient supabase;
8 |
9 | DataInitRepository(this.supabase);
10 |
11 | Future> fetchIdioms() async {
12 | logger('Now fetching idioms');
13 | try {
14 | final idiomsData = await Supabase.instance.client.from('idioms').select();
15 | if (idiomsData.isNotEmpty) {
16 | final idioms =
17 | (idiomsData as List).map((item) => Idiom.fromJson(item)).toList();
18 | logger('${idioms.length} idioms fetched');
19 | return idioms;
20 | }
21 | } catch (e) {
22 | logger('Unable to fetch idioms: $e');
23 | }
24 | return [];
25 | }
26 |
27 | Future> fetchProverbs() async {
28 | logger('Now fetching proverbs');
29 | try {
30 | final proverbsData = await supabase.from('proverbs').select();
31 | if (proverbsData.isNotEmpty) {
32 | final proverbs = (proverbsData as List)
33 | .map((item) => Proverb.fromJson(item))
34 | .toList();
35 | logger('${proverbs.length} proverbs fetched');
36 | return proverbs;
37 | }
38 | } catch (e) {
39 | logger('Unable to fetch proverbs: $e');
40 | }
41 | return [];
42 | }
43 |
44 | Future> fetchSayings() async {
45 | logger('Now fetching sayings');
46 | try {
47 | final sayingsData = await supabase.from('sayings').select();
48 | if (sayingsData.isNotEmpty) {
49 | final sayings =
50 | (sayingsData as List).map((item) => Saying.fromJson(item)).toList();
51 | logger('${sayings.length} sayings fetched');
52 | return sayings;
53 | }
54 | } catch (e) {
55 | logger('Unable to fetch sayings: $e');
56 | }
57 | return [];
58 | }
59 |
60 | Future> fetchWords() async {
61 | logger('Now fetching words');
62 | try {
63 | final wordsData = await supabase.from('words').select();
64 | if (wordsData.isNotEmpty) {
65 | final words =
66 | (wordsData as List).map((item) => Word.fromJson(item)).toList();
67 | logger('${words.length} words fetched');
68 | return words;
69 | }
70 | } catch (e) {
71 | logger('Unable to fetch words: $e');
72 | }
73 | return [];
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/presentation/screens/home/search/word_search.dart:
--------------------------------------------------------------------------------
1 | part of '../home_screen.dart';
2 |
3 | class WordSearch extends SearchDelegate {
4 | final List words;
5 |
6 | WordSearch(BuildContext context, this.words);
7 |
8 | @override
9 | String get searchFieldLabel => "Tafuta Neno";
10 |
11 | @override
12 | ThemeData appBarTheme(BuildContext context) {
13 | return Theme.of(context).brightness == Brightness.light
14 | ? AppTheme.darkTheme()
15 | : AppTheme.lightTheme();
16 | }
17 |
18 | @override
19 | List buildActions(BuildContext context) {
20 | return [
21 | IconButton(
22 | onPressed: () => query = '',
23 | icon: const Icon(Icons.clear, color: Colors.white),
24 | ),
25 | ];
26 | }
27 |
28 | @override
29 | Widget buildLeading(BuildContext context) {
30 | return IconButton(
31 | onPressed: () => close(context, words),
32 | icon: const Icon(Icons.arrow_back, color: Colors.white),
33 | );
34 | }
35 |
36 | @override
37 | Widget buildResults(BuildContext context) => _buildSearchResults(context);
38 |
39 | @override
40 | Widget buildSuggestions(BuildContext context) => _buildSearchResults(context);
41 |
42 | Widget _buildSearchResults(BuildContext context) {
43 | final filteredWords = _filterWords(query);
44 |
45 | if (filteredWords.isEmpty) {
46 | return const Center(
47 | child: Text(
48 | "Hakuna matokeo",
49 | style: TextStyle(fontSize: 18, color: Colors.grey),
50 | ),
51 | );
52 | }
53 |
54 | return ListView.builder(
55 | padding: const EdgeInsets.all(10),
56 | itemCount: filteredWords.length,
57 | itemBuilder: (context, index) {
58 | final word = filteredWords[index];
59 | return WordItem(
60 | word: word,
61 | onTap: () {
62 | Navigator.push(
63 | context,
64 | MaterialPageRoute(
65 | builder: (context) => WordScreen(
66 | word: word,
67 | words: words,
68 | ),
69 | ),
70 | );
71 | },
72 | );
73 | },
74 | );
75 | }
76 |
77 | List _filterWords(String query) {
78 | if (query.trim().isEmpty) return words;
79 |
80 | final lowerQuery = query.trim().toLowerCase();
81 |
82 | return words.where((word) {
83 | return (word.title?.toLowerCase().contains(lowerQuery) ?? false);
84 | }).toList();
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/lib/presentation/theme/theme_data.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../../data/repository/pref_repository.dart';
4 | import '../../core/utils/app_util.dart';
5 | import '../../core/di/injectable.dart';
6 | import 'theme_colors.dart';
7 |
8 | class AppTheme {
9 | AppTheme._();
10 |
11 | static String currentTheme() {
12 | var prefRepo = getIt();
13 | return getThemeModeString(prefRepo.getThemeMode());
14 | }
15 |
16 | static ThemeData lightTheme() {
17 | return ThemeData(
18 | scaffoldBackgroundColor: ThemeColors.accent1,
19 | colorScheme: const ColorScheme(
20 | brightness: Brightness.light,
21 | primary: ThemeColors.primary,
22 | onPrimary: Colors.white,
23 | primaryContainer: ThemeColors.primary,
24 | secondary: ThemeColors.primary1,
25 | onSecondary: Colors.grey,
26 | secondaryContainer: ThemeColors.primary1,
27 | surface: Colors.white,
28 | onSurface: Colors.black,
29 | error: Colors.red,
30 | onError: Colors.white,
31 | ),
32 | appBarTheme: const AppBarTheme(
33 | backgroundColor: ThemeColors.primary,
34 | foregroundColor: ThemeColors.accent,
35 | elevation: 3,
36 | ),
37 | navigationBarTheme: const NavigationBarThemeData(
38 | backgroundColor: Colors.white,
39 | indicatorColor: ThemeColors.accent,
40 | elevation: 3,
41 | ),
42 | );
43 | }
44 |
45 | static ThemeData darkTheme() {
46 | return ThemeData(
47 | scaffoldBackgroundColor: ThemeColors.primaryDark2,
48 | colorScheme: const ColorScheme(
49 | brightness: Brightness.light,
50 | primary: ThemeColors.primary2,
51 | onPrimary: ThemeColors.primaryDark1,
52 | primaryContainer: ThemeColors.primaryDark,
53 | secondary: ThemeColors.primaryDark1,
54 | onSecondary: ThemeColors.primaryDark1,
55 | secondaryContainer: ThemeColors.primaryDark1,
56 | surface: ThemeColors.primaryDark,
57 | onSurface: Colors.white,
58 | error: Colors.red,
59 | onError: Colors.white,
60 | ),
61 | appBarTheme: const AppBarTheme(
62 | backgroundColor: ThemeColors.primaryDark1,
63 | foregroundColor: Colors.white,
64 | elevation: 3,
65 | ),
66 | navigationBarTheme: const NavigationBarThemeData(
67 | backgroundColor: Colors.white,
68 | indicatorColor: ThemeColors.accent,
69 | elevation: 3,
70 | ),
71 | );
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/lib/presentation/blocs/home/home_bloc.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter_bloc/flutter_bloc.dart';
4 | import 'package:freezed_annotation/freezed_annotation.dart';
5 | import 'package:package_info_plus/package_info_plus.dart';
6 |
7 | import '../../../data/models/models.dart';
8 | import '../../../core/utils/app_util.dart';
9 | import '../../../data/repository/db/database_repository.dart';
10 | import '../../../core/di/injectable.dart';
11 | import '../../../domain/home_repository.dart';
12 |
13 | part 'home_event.dart';
14 | part 'home_state.dart';
15 |
16 | part 'home_bloc.freezed.dart';
17 |
18 | class HomeBloc extends Bloc {
19 | HomeBloc() : super(const _HomeState()) {
20 | on(_onFetchData);
21 | on(_onCheckAppUpdates);
22 | }
23 |
24 | final _dbRepo = getIt();
25 | final _homeRepo = HomeRepository();
26 |
27 | void _onCheckAppUpdates(
28 | CheckAppUpdates event,
29 | Emitter emit,
30 | ) async {
31 | emit(const HomeProgressState());
32 | AppUpdate? appUpdate;
33 | PackageInfo packageInfo = await PackageInfo.fromPlatform();
34 | String currentVersion = packageInfo.version;
35 | var resp = await _homeRepo.fetchUpdateInfo();
36 |
37 | try {
38 | switch (resp.statusCode) {
39 | case 200:
40 | appUpdate = AppUpdate.fromJson(jsonDecode(resp.body));
41 | if (isNewerVersion(currentVersion, appUpdate.version!)) {
42 | logger("We need to upgrade from v$currentVersion to ${appUpdate.version}");
43 | emit(HomeUpdateAppState(true, appUpdate));
44 | } else {
45 | logger("No never version found, sticking to $currentVersion");
46 | emit(HomeUpdateAppState(false, appUpdate));
47 | }
48 | break;
49 |
50 | default:
51 | logger("Error finding new update info: ${resp.statusCode}");
52 | emit(HomeUpdateAppState(false, appUpdate!));
53 | break;
54 | }
55 | } catch (e) {
56 | logger("Error finding new update info: $e");
57 | emit(HomeUpdateAppState(false, appUpdate!));
58 | }
59 | }
60 |
61 | void _onFetchData(
62 | FetchData event,
63 | Emitter emit,
64 | ) async {
65 | emit(const HomeProgressState());
66 | List idioms = [];
67 | List proverbs = [];
68 | List sayings = [];
69 | List words = [];
70 |
71 | try {
72 | idioms = await _dbRepo.fetchIdioms();
73 | proverbs = await _dbRepo.fetchProverbs();
74 | sayings = await _dbRepo.fetchSayings();
75 | words = await _dbRepo.fetchWords();
76 | emit(HomeFetchedDataState(idioms, proverbs, sayings, words));
77 | } catch (e) {
78 | logger("Error log: $e");
79 | emit(HomeFetchedDataState(idioms, proverbs, sayings, words));
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/lib/presentation/screens/home/widgets/home_appbar.dart:
--------------------------------------------------------------------------------
1 | part of '../home_screen.dart';
2 |
3 | class HomeAppBar extends StatefulWidget implements PreferredSizeWidget {
4 | final HomeScreenState parent;
5 | const HomeAppBar({super.key, required this.parent});
6 |
7 | @override
8 | HomeAppBarState createState() => HomeAppBarState();
9 |
10 | @override
11 | Size get preferredSize => const Size.fromHeight(100);
12 | }
13 |
14 | class HomeAppBarState extends State {
15 | late HomeScreenState parent;
16 |
17 | @override
18 | Widget build(BuildContext context) {
19 | parent = widget.parent;
20 | var tabActions = PreferredSize(
21 | preferredSize: const Size.fromHeight(50),
22 | child: SizedBox(
23 | height: 50,
24 | child: ListView.builder(
25 | shrinkWrap: true,
26 | scrollDirection: Axis.horizontal,
27 | itemCount: AppConstants.filters.length,
28 | itemBuilder: (context, index) {
29 | return TabActionItem(
30 | index: index,
31 | isSelected: parent.setPage == index,
32 | onTapped: () => parent.onTabChanged(index),
33 | );
34 | },
35 | ),
36 | ),
37 | );
38 |
39 | return AppBar(
40 | centerTitle: true,
41 | title: Text(
42 | '${AppConstants.appTitle} - ${AppConstants.appTitle1}',
43 | style: TextStyles.headingStyle1.bold.size(25),
44 | ),
45 | bottom: tabActions,
46 | );
47 | }
48 | }
49 |
50 | class TabActionItem extends StatelessWidget {
51 | final int? index;
52 | final bool isSelected;
53 | final VoidCallback? onTapped;
54 |
55 | const TabActionItem({
56 | super.key,
57 | required this.index,
58 | this.isSelected = false,
59 | this.onTapped,
60 | });
61 |
62 | @override
63 | Widget build(BuildContext context) {
64 | return Padding(
65 | padding: const EdgeInsets.symmetric(horizontal: 5),
66 | child: RawMaterialButton(
67 | fillColor: isSelected
68 | ? ThemeColors.bgColorAccent2(context)
69 | : ThemeColors.bgColorPrimary5(context),
70 | highlightColor: Colors.white,
71 | focusElevation: 0,
72 | hoverColor: ThemeColors.primaryDark,
73 | hoverElevation: 1,
74 | highlightElevation: 0,
75 | elevation: 0,
76 | padding: EdgeInsets.zero,
77 | shape: const RoundedRectangleBorder(
78 | borderRadius: BorderRadius.only(
79 | topRight: Radius.circular(10),
80 | topLeft: Radius.circular(10),
81 | ),
82 | ),
83 | onPressed: onTapped,
84 | child: Text(
85 | AppConstants.filters[index!].toUpperCase(),
86 | style: TextStyles.buttonTextStyle.bold
87 | .textColor(isSelected ? ThemeColors.primary : Colors.white),
88 | ),
89 | ),
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "40.png",
5 | "idiom" : "iphone",
6 | "scale" : "2x",
7 | "size" : "20x20"
8 | },
9 | {
10 | "filename" : "60.png",
11 | "idiom" : "iphone",
12 | "scale" : "3x",
13 | "size" : "20x20"
14 | },
15 | {
16 | "filename" : "29.png",
17 | "idiom" : "iphone",
18 | "scale" : "1x",
19 | "size" : "29x29"
20 | },
21 | {
22 | "filename" : "58.png",
23 | "idiom" : "iphone",
24 | "scale" : "2x",
25 | "size" : "29x29"
26 | },
27 | {
28 | "filename" : "87.png",
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "29x29"
32 | },
33 | {
34 | "filename" : "80.png",
35 | "idiom" : "iphone",
36 | "scale" : "2x",
37 | "size" : "40x40"
38 | },
39 | {
40 | "filename" : "120.png",
41 | "idiom" : "iphone",
42 | "scale" : "3x",
43 | "size" : "40x40"
44 | },
45 | {
46 | "filename" : "120 1.png",
47 | "idiom" : "iphone",
48 | "scale" : "2x",
49 | "size" : "60x60"
50 | },
51 | {
52 | "filename" : "180.png",
53 | "idiom" : "iphone",
54 | "scale" : "3x",
55 | "size" : "60x60"
56 | },
57 | {
58 | "filename" : "20.png",
59 | "idiom" : "ipad",
60 | "scale" : "1x",
61 | "size" : "20x20"
62 | },
63 | {
64 | "filename" : "40 1.png",
65 | "idiom" : "ipad",
66 | "scale" : "2x",
67 | "size" : "20x20"
68 | },
69 | {
70 | "filename" : "29 1.png",
71 | "idiom" : "ipad",
72 | "scale" : "1x",
73 | "size" : "29x29"
74 | },
75 | {
76 | "filename" : "58 1.png",
77 | "idiom" : "ipad",
78 | "scale" : "2x",
79 | "size" : "29x29"
80 | },
81 | {
82 | "filename" : "40 2.png",
83 | "idiom" : "ipad",
84 | "scale" : "1x",
85 | "size" : "40x40"
86 | },
87 | {
88 | "filename" : "80 1.png",
89 | "idiom" : "ipad",
90 | "scale" : "2x",
91 | "size" : "40x40"
92 | },
93 | {
94 | "filename" : "76.png",
95 | "idiom" : "ipad",
96 | "scale" : "1x",
97 | "size" : "76x76"
98 | },
99 | {
100 | "filename" : "152.png",
101 | "idiom" : "ipad",
102 | "scale" : "2x",
103 | "size" : "76x76"
104 | },
105 | {
106 | "filename" : "167.png",
107 | "idiom" : "ipad",
108 | "scale" : "2x",
109 | "size" : "83.5x83.5"
110 | },
111 | {
112 | "filename" : "1024.png",
113 | "idiom" : "ios-marketing",
114 | "scale" : "1x",
115 | "size" : "1024x1024"
116 | }
117 | ],
118 | "info" : {
119 | "author" : "xcode",
120 | "version" : 1
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/presentation/screens/viewer/word_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:flutter_html/flutter_html.dart';
4 | import 'package:textstyle_extensions/textstyle_extensions.dart';
5 |
6 | import '../../../data/models/models.dart';
7 | import '../../../core/utils/constants/app_constants.dart';
8 | import '../../widgets/progress/custom_snackbar.dart';
9 | import '../../widgets/progress/general_progress.dart';
10 | import '../../theme/theme_colors.dart';
11 | import '../../theme/theme_fonts.dart';
12 | import '../../blocs/viewer/viewer_bloc.dart';
13 |
14 | part 'widgets/word_details.dart';
15 | part 'word_view.dart';
16 |
17 | class WordScreen extends StatefulWidget {
18 | final Word word;
19 | final List words;
20 |
21 | const WordScreen({
22 | super.key,
23 | required this.word,
24 | required this.words,
25 | });
26 |
27 | @override
28 | State createState() => WordScreenState();
29 | }
30 |
31 | class WordScreenState extends State {
32 | late Word word;
33 | IconData likeIcon = Icons.favorite_border;
34 | bool isLiked = false, likeChanged = false;
35 |
36 | @override
37 | Widget build(BuildContext context) {
38 | word = widget.word;
39 |
40 | return BlocProvider(
41 | create: (context) => ViewerBloc()..add(LoadWord(widget.word)),
42 | child: BlocConsumer(
43 | listener: (context, state) {
44 | if (state is ViewerFailureState) {
45 | CustomSnackbar.show(context, state.feedback);
46 | } else if (state is ViewerLikedState) {
47 | setState(() {
48 | //word.liked = !word.liked;
49 | likeChanged = true;
50 | });
51 | if (state.liked) {
52 | CustomSnackbar.show(
53 | context,
54 | '${word.title} added to your likes',
55 | isSuccess: true,
56 | );
57 | } else {
58 | CustomSnackbar.show(
59 | context,
60 | '${word.title} removed from your likes',
61 | );
62 | }
63 | }
64 | },
65 | builder: (context, state) {
66 | return Scaffold(
67 | appBar: AppBar(
68 | title: const Text(
69 | '${AppConstants.appTitle} - ${AppConstants.appTitle1}',
70 | ),
71 | ),
72 | body: state.maybeWhen(
73 | failure: (feedback) => EmptyState(title: feedback),
74 | orElse: () => const EmptyState(title: 'Hamna chochote hapa'),
75 | progress: () => const LoadingProgress(title: "Inapakia data ..."),
76 | loaded: (meanings, synonyms) {
77 | return WordView(
78 | word: word,
79 | meanings: meanings,
80 | synonyms: synonyms,
81 | );
82 | },
83 | ),
84 | );
85 | },
86 | ),
87 | );
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/linux/flutter/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | # This file controls Flutter-level build steps. It should not be edited.
2 | cmake_minimum_required(VERSION 3.10)
3 |
4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral")
5 |
6 | # Configuration provided via flutter tool.
7 | include(${EPHEMERAL_DIR}/generated_config.cmake)
8 |
9 | # TODO: Move the rest of this into files in ephemeral. See
10 | # https://github.com/flutter/flutter/issues/57146.
11 |
12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...),
13 | # which isn't available in 3.10.
14 | function(list_prepend LIST_NAME PREFIX)
15 | set(NEW_LIST "")
16 | foreach(element ${${LIST_NAME}})
17 | list(APPEND NEW_LIST "${PREFIX}${element}")
18 | endforeach(element)
19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE)
20 | endfunction()
21 |
22 | # === Flutter Library ===
23 | # System-level dependencies.
24 | find_package(PkgConfig REQUIRED)
25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0)
26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0)
28 |
29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so")
30 |
31 | # Published to parent scope for install step.
32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE)
33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE)
34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE)
35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE)
36 |
37 | list(APPEND FLUTTER_LIBRARY_HEADERS
38 | "fl_basic_message_channel.h"
39 | "fl_binary_codec.h"
40 | "fl_binary_messenger.h"
41 | "fl_dart_project.h"
42 | "fl_engine.h"
43 | "fl_json_message_codec.h"
44 | "fl_json_method_codec.h"
45 | "fl_message_codec.h"
46 | "fl_method_call.h"
47 | "fl_method_channel.h"
48 | "fl_method_codec.h"
49 | "fl_method_response.h"
50 | "fl_plugin_registrar.h"
51 | "fl_plugin_registry.h"
52 | "fl_standard_message_codec.h"
53 | "fl_standard_method_codec.h"
54 | "fl_string_codec.h"
55 | "fl_value.h"
56 | "fl_view.h"
57 | "flutter_linux.h"
58 | )
59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/")
60 | add_library(flutter INTERFACE)
61 | target_include_directories(flutter INTERFACE
62 | "${EPHEMERAL_DIR}"
63 | )
64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}")
65 | target_link_libraries(flutter INTERFACE
66 | PkgConfig::GTK
67 | PkgConfig::GLIB
68 | PkgConfig::GIO
69 | )
70 | add_dependencies(flutter flutter_assemble)
71 |
72 | # === Flutter tool backend ===
73 | # _phony_ is a non-existent file to force this command to run every time,
74 | # since currently there's no way to get a full input/output list from the
75 | # flutter tool.
76 | add_custom_command(
77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS}
78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_
79 | COMMAND ${CMAKE_COMMAND} -E env
80 | ${FLUTTER_TOOL_ENVIRONMENT}
81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh"
82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE}
83 | VERBATIM
84 | )
85 | add_custom_target(flutter_assemble DEPENDS
86 | "${FLUTTER_LIBRARY}"
87 | ${FLUTTER_LIBRARY_HEADERS}
88 | )
89 |
--------------------------------------------------------------------------------
/lib/presentation/screens/viewer/widgets/word_details.dart:
--------------------------------------------------------------------------------
1 | part of '../word_screen.dart';
2 |
3 | class WordDetails extends StatelessWidget {
4 | final Word word;
5 | final List meanings;
6 | final List synonyms;
7 |
8 | const WordDetails({
9 | super.key,
10 | required this.word,
11 | required this.meanings,
12 | required this.synonyms,
13 | });
14 |
15 | @override
16 | Widget build(BuildContext context) {
17 | return Column(
18 | crossAxisAlignment: CrossAxisAlignment.start,
19 | children: [
20 | if (meanings.isNotEmpty) MeaningCard(meanings: meanings),
21 | if (synonyms.isNotEmpty) ...[
22 | const SizedBox(height: 15),
23 | Text(
24 | 'Visawe',
25 | style: TextStyles.headingStyle2.bold
26 | .size(25)
27 | .italic
28 | .textColor(ThemeColors.primary),
29 | ),
30 | ListView.builder(
31 | shrinkWrap: true,
32 | physics: const NeverScrollableScrollPhysics(),
33 | itemCount: synonyms.length,
34 | itemBuilder: (context, index) =>
35 | SynonymCard(synonym: synonyms[index]),
36 | ),
37 | ],
38 | ],
39 | );
40 | }
41 | }
42 |
43 | class MeaningCard extends StatelessWidget {
44 | final List meanings;
45 |
46 | const MeaningCard({super.key, required this.meanings});
47 |
48 | @override
49 | Widget build(BuildContext context) {
50 | final String formattedHtml = meanings.asMap().entries.map((entry) {
51 | final parts = entry.value.split(":");
52 | return parts.length > 1
53 | ? "${parts.first}
Mfano: ${parts.last}"
54 | : "${parts.first}";
55 | }).join();
56 |
57 | return Card(
58 | elevation: 2,
59 | color: Colors.white,
60 | child: Html(
61 | data: "$formattedHtml
",
62 | style: {
63 | "ol": Style(
64 | fontSize: FontSize(18),
65 | color: ThemeColors.primary,
66 | ),
67 | "li": Style(
68 | margin: Margins.only(bottom: 10),
69 | ),
70 | },
71 | ),
72 | );
73 | }
74 | }
75 |
76 | class SynonymCard extends StatelessWidget {
77 | final String synonym;
78 |
79 | const SynonymCard({super.key, required this.synonym});
80 |
81 | @override
82 | Widget build(BuildContext context) {
83 | final TextStyle bodyTxtStyle =
84 | TextStyles.bodyStyle1.size(20).textColor(ThemeColors.primary);
85 |
86 | return Card(
87 | elevation: 2,
88 | color: Colors.white,
89 | child: Padding(
90 | padding: const EdgeInsets.all(10),
91 | child: Row(
92 | children: [
93 | const Icon(Icons.arrow_circle_right, color: ThemeColors.primary),
94 | const SizedBox(width: 10),
95 | Expanded(
96 | child: Text(synonym, style: bodyTxtStyle),
97 | ),
98 | ],
99 | ),
100 | ),
101 | );
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/lib/presentation/theme/theme_styles.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | class AppDurations {
5 | static const Duration fastest = Duration(seconds: 1);
6 |
7 | static const Duration fast = Duration(seconds: 2);
8 |
9 | static const Duration medium = Duration(seconds: 3);
10 |
11 | static const Duration slow = Duration(seconds: 5);
12 | }
13 |
14 | class Sizes {
15 | /// extra small size = 5
16 | static const double xs = 5;
17 |
18 | /// small size = 10
19 | static const double sm = 10;
20 |
21 | /// medium size = 15
22 | static const double m = 15;
23 |
24 | /// large size = 20
25 | static const double l = 20;
26 |
27 | /// extra large size = 30
28 | static const double xl = 30;
29 |
30 | /// extra extra large size = 50
31 | static const double xxl = 50;
32 | }
33 |
34 | class ThemeFonts {
35 | static const String poppins = "Poppins";
36 | }
37 |
38 | class Insets {
39 | static double gutterScale = 1;
40 |
41 | static const double scale = 1;
42 |
43 | /// Dynamic insets, may get scaled with the device size
44 | static double mGutter = m * gutterScale;
45 |
46 | static double lGutter = l * gutterScale;
47 |
48 | static const double xs = 2 * scale;
49 | static const double sm = 6 * scale;
50 | static const double m = 12 * scale;
51 | static const double l = 24 * scale;
52 | static const double xl = 36 * scale;
53 | }
54 |
55 | class Shadows {
56 | static bool enabled = true;
57 |
58 | static double get mRadius => 8;
59 |
60 | static List m(Color color, [double opacity = 0]) {
61 | return enabled
62 | ? [
63 | BoxShadow(
64 | color: color.withOpacity(opacity),
65 | blurRadius: mRadius,
66 | spreadRadius: mRadius / 2,
67 | offset: const Offset(1, 0),
68 | ),
69 | BoxShadow(
70 | color: color.withOpacity(opacity),
71 | blurRadius: mRadius / 2,
72 | spreadRadius: mRadius / 4,
73 | offset: const Offset(1, 0),
74 | )
75 | ]
76 | : const [];
77 | }
78 | }
79 |
80 | class Corners {
81 | static const double btn = s5;
82 |
83 | static const double dialog = 12;
84 |
85 | /// Xs
86 | static const double s3 = 3;
87 |
88 | static BorderRadius get s3Border => BorderRadius.all(s3Radius);
89 |
90 | static Radius get s3Radius => const Radius.circular(s3);
91 |
92 | /// Small
93 | static const double s5 = 5;
94 |
95 | static BorderRadius get s5Border => BorderRadius.all(s5Radius);
96 |
97 | static Radius get s5Radius => const Radius.circular(s5);
98 |
99 | /// Medium
100 | static const double s8 = 8;
101 |
102 | static const BorderRadius s8Border = BorderRadius.all(s8Radius);
103 |
104 | static const Radius s8Radius = Radius.circular(s8);
105 |
106 | /// Large
107 | static const double s10 = 10;
108 |
109 | static BorderRadius get s10Border => BorderRadius.all(s10Radius);
110 |
111 | static Radius get s10Radius => const Radius.circular(s10);
112 | }
113 |
--------------------------------------------------------------------------------
/lib/presentation/screens/home/widgets/update_now.dart:
--------------------------------------------------------------------------------
1 | part of '../home_screen.dart';
2 |
3 | class UpdateNow extends StatelessWidget {
4 | final AppUpdate update;
5 | const UpdateNow({
6 | super.key,
7 | required this.update,
8 | });
9 |
10 | Future launchPlayStore() async {
11 | String appUrl = '';
12 | if (Platform.isAndroid) {
13 | appUrl = update.appLinks!.android!;
14 | } else if (Platform.isIOS) {
15 | appUrl = update.appLinks!.ios!;
16 | } else if (Platform.isWindows) {
17 | appUrl = update.appLinks!.windows!;
18 | } else if (Platform.isMacOS) {
19 | appUrl = update.appLinks!.macos!;
20 | } else if (Platform.isLinux) {
21 | appUrl = update.appLinks!.linux!;
22 | }
23 | if (!await launchUrl(Uri.parse(appUrl))) {
24 | throw Exception('Could not launch updater');
25 | }
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | return Padding(
31 | padding: const EdgeInsets.all(20),
32 | child: Card(
33 | child: Padding(
34 | padding: const EdgeInsets.all(20),
35 | child: Column(
36 | mainAxisAlignment: MainAxisAlignment.center,
37 | children: [
38 | [
39 | const Padding(
40 | padding: EdgeInsets.all(10),
41 | child: Icon(
42 | Icons.system_update,
43 | size: 25,
44 | color: ThemeColors.primary,
45 | ),
46 | ),
47 | const Text(
48 | 'There\'s a new update',
49 | style: TextStyle(
50 | color: ThemeColors.primary,
51 | fontSize: 22,
52 | fontWeight: FontWeight.w600,
53 | ),
54 | ),
55 | ].toRow(
56 | mainAxisAlignment: MainAxisAlignment.center,
57 | ),
58 | const SizedBox(height: 10),
59 | Text(
60 | 'Version: ${update.version}',
61 | style: TextStyles.bodyStyle1.size(20).textHeight(1.2),
62 | ),
63 | const SizedBox(height: 30),
64 | const Text(
65 | "What's new:",
66 | style: TextStyle(
67 | color: ThemeColors.primary,
68 | fontSize: 25,
69 | fontWeight: FontWeight.w600,
70 | ),
71 | ),
72 | Text(
73 | update.description!,
74 | textAlign: TextAlign.center,
75 | style: TextStyles.bodyStyle1.size(18).textHeight(1.2),
76 | ),
77 | const SizedBox(height: 20),
78 | AppButton(
79 | label: 'UPDATE NOW',
80 | onPressed: launchPlayStore,
81 | bgColor: ThemeColors.primary,
82 | foreColor: Colors.white,
83 | hoverColor: Colors.red,
84 | padding:
85 | const EdgeInsets.symmetric(vertical: 10, horizontal: 30),
86 | borderRadius: const BorderRadius.all(Radius.circular(10)),
87 | ).center()
88 | ],
89 | ),
90 | ),
91 | ),
92 | );
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SwahiLib - Kamusi ya Kiswahili
2 | The Kamusi ya Kiswahili app has been rebranded to SwahiLib. If you are looking for the old app you can find it in the [old-app branch](https://github.com/oyonde/SwahiLib/tree/old-app)
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | Swahilib - Kamusi ya Kiswahili for Android, iOS
14 |
15 | ## Getting Started
16 |
17 | Follow this guide to set up and run the SwahiLib App:
18 |
19 | ## Getting Started
20 |
21 | Follow this guide to set up and run SwahiLib:
22 |
23 | ### Setting Up SwahiLib:
24 |
25 | 1. **Install Flutter and Dependencies:** Ensure Flutter is installed on your system. Download the Flutter SDK from the official website and set up your preferred IDE (e.g., Android Studio or Visual Studio Code) with the Flutter plugin.
26 |
27 | 2. **Clone the Repository:** Clone SwahiLib repository from GitHub using Git:
28 |
29 | ```bash
30 | git clone git@github.com:SiroDaves/SwahiLibApp.git
31 | ```
32 |
33 | 3. **Install Packages:** Navigate to the project directory and run:
34 |
35 | ```bash
36 | flutter pub get
37 | ```
38 |
39 | ### Running SwahiLib:
40 |
41 | 1. **Device Setup:** Connect an emulator or physical device to your development environment. Check connected devices:
42 |
43 | ```bash
44 | flutter devices
45 | ```
46 |
47 | 2. **Update Dependencies:**
48 |
49 | ```bash
50 | flutter pub get
51 | ```
52 |
53 | 3. **Update Code Generated Files:**
54 |
55 | ```bash
56 | dart run build_runner build --delete-conflicting-outputs
57 | ```
58 |
59 | 4. **Update Localization Strings:**
60 |
61 | ```bash
62 | flutter gen-l10n
63 | ```
64 | 5. **Running SwahiLib:**
65 | ```bash
66 | flutter run --flavor develop -t lib/main_dev.dart --dart-define-from-file keys-dev.json
67 | ```
68 |
69 | ### Building SwahiLib
70 |
71 | 1. **Android:**
72 |
73 | - **Production (For Play Store):**
74 |
75 | ```bash
76 | flutter build appbundle --flavor production -t lib/main.dart --dart-define-from-file keys-prod.json --no-tree-shake-icons
77 | ```
78 |
79 | 2. **iOS:**
80 |
81 | - **Production (For Play Store):**
82 |
83 | ```bash
84 | flutter build ipa -t lib/main.dart --dart-define-from-file keys-prod.json --no-tree-shake-icons
85 | ```
86 | To upload to the App Store either:
87 | - Drag and drop the "build/ios/ipa/*.ipa" bundle into the Apple Transporter macOS app https://apps.apple.com/us/app/transporter/id1450874784
88 | - Run "xcrun altool --upload-app --type ios -f build/ios/ipa/*.ipa --apiKey your_api_key --apiIssuer your_issuer_id".
89 | See "man altool" for details about how to authenticate with the App Store Connect API key.
90 |
91 | ---
92 |
93 | Congratulations! You've successfully set up and run or built SwahiLib. Explore the codebase, make modifications, and contribute to creating a seamless experience for the users. Happy coding!
--------------------------------------------------------------------------------
/lib/presentation/theme/theme_fonts.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/cupertino.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:textstyle_extensions/textstyle_extensions.dart';
4 |
5 | class ThemeFonts {
6 |
7 | static const themeFonts = 'TrebuchetMS';
8 |
9 | static const title = themeFonts;
10 | static const body = themeFonts;
11 | static const button = themeFonts;
12 | }
13 |
14 | class FontSizes {
15 | /// font size 10
16 | static const double s10 = 10;
17 |
18 | /// font size 12
19 | static const double s12 = 12;
20 |
21 | /// font size 14
22 | static const double s14 = 14;
23 |
24 | /// font size 16
25 | static const double s16 = 16;
26 |
27 | /// font size 18
28 | static const double s18 = 18;
29 |
30 | /// font size 20
31 | static const double s20 = 20;
32 |
33 | /// font size 22
34 | static const double s22 = 22;
35 |
36 | /// font size 25
37 | static const double s25 = 25;
38 |
39 | /// font size 30
40 | static const double s30 = 30;
41 | }
42 |
43 | class TextStyles {
44 | static const TextStyle trebuchetMS = TextStyle(
45 | fontFamily: ThemeFonts.themeFonts,
46 | fontWeight: FontWeight.w400,
47 | letterSpacing: 0,
48 | height: 1,
49 | fontFamilyFallback: [
50 | ThemeFonts.themeFonts,
51 | ],
52 | );
53 |
54 | static TextStyle get pageTitle1 =>
55 | trebuchetMS.bold.size(FontSizes.s30).letterSpace(2);
56 |
57 | static TextStyle get headingStyle1 =>
58 | trebuchetMS.bold.size(FontSizes.s22).letterSpace(.5);
59 |
60 | static TextStyle get headingStyle2 =>
61 | trebuchetMS.bold.size(FontSizes.s20).letterSpace(.3);
62 |
63 | static TextStyle get headingStyle3 => trebuchetMS.bold.size(FontSizes.s18);
64 |
65 | static TextStyle get headingStyle4 => trebuchetMS.bold.size(FontSizes.s16);
66 |
67 | static TextStyle get headingStyle5 => trebuchetMS.bold.size(FontSizes.s14);
68 |
69 | static TextStyle get headingStyle6 => trebuchetMS.bold.size(FontSizes.s12);
70 |
71 | static TextStyle get bodyStyle1 => trebuchetMS.size(FontSizes.s16);
72 |
73 | static TextStyle get bodyStyle2 => trebuchetMS.size(FontSizes.s14);
74 |
75 | static TextStyle get bodyStyle3 => trebuchetMS.size(FontSizes.s12);
76 |
77 | static TextStyle get callOut => trebuchetMS.size(FontSizes.s20).letterSpace(1.5).bold;
78 |
79 | static TextStyle get callOutFocus => callOut.bold;
80 |
81 | static TextStyle get buttonTextStyle =>
82 | trebuchetMS.bold.size(FontSizes.s14).letterSpace(1.75);
83 |
84 | static TextStyle get buttonSelected =>
85 | trebuchetMS.size(FontSizes.s14).letterSpace(1.75);
86 |
87 | static TextStyle get footNote => trebuchetMS.bold.size(FontSizes.s10);
88 |
89 | static TextStyle get captionText =>
90 | trebuchetMS.size(FontSizes.s10).letterSpace(.3);
91 |
92 | static TextStyle get titleStyle12 =>
93 | const TextStyle(fontWeight: FontWeight.bold, fontSize: 12);
94 | static TextStyle get titleStyle14 =>
95 | const TextStyle(fontWeight: FontWeight.bold, fontSize: 14);
96 | static TextStyle get titleStyle22 =>
97 | const TextStyle(fontWeight: FontWeight.bold, fontSize: 22);
98 | static TextStyle get titleStyle20 =>
99 | const TextStyle(fontWeight: FontWeight.bold, fontSize: 20);
100 |
101 | static TextStyle get labelStyle16 =>
102 | const TextStyle(fontWeight: FontWeight.bold, fontSize: 16);
103 | }
104 |
--------------------------------------------------------------------------------
/lib/data/repository/pref_repository.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:injectable/injectable.dart';
3 | import 'package:shared_preferences/shared_preferences.dart';
4 |
5 | import '../models/word.dart';
6 | import '../../core/utils/constants/pref_constants.dart';
7 |
8 | @singleton
9 | abstract class PrefRepository {
10 | @factoryMethod
11 | factory PrefRepository(SharedPreferences prefRepo) = PrefRepositoryImp;
12 |
13 | ThemeMode getThemeMode();
14 |
15 | List? words = [];
16 |
17 | Future updateThemeMode(ThemeMode themeMode);
18 |
19 | bool getPrefBool(String settingsKey);
20 | int getPrefInt(String settingsKey);
21 | String getPrefString(String settingsKey);
22 |
23 | void setPrefBool(String settingsKey, bool settingsValue);
24 | void setPrefInt(String settingsKey, int settingsValue);
25 | void setPrefString(String settingsKey, String settingsValue);
26 |
27 | void clearData();
28 | void removeKeyPair(String settingsKey);
29 | bool keyExists(String settingsKey);
30 | }
31 |
32 | class PrefRepositoryImp implements PrefRepository {
33 | final SharedPreferences sharedPrefs;
34 |
35 | PrefRepositoryImp(this.sharedPrefs);
36 |
37 | @override
38 | Future updateThemeMode(ThemeMode themeMode) async {
39 | await sharedPrefs.setString(
40 | PrefConstants.appThemeKey,
41 | themeMode.toString(),
42 | );
43 | }
44 |
45 | @override
46 | ThemeMode getThemeMode() {
47 | switch (sharedPrefs.getString(PrefConstants.appThemeKey)) {
48 | case 'ThemeMode.light':
49 | return ThemeMode.light;
50 |
51 | case 'ThemeMode.dark':
52 | return ThemeMode.dark;
53 |
54 | default:
55 | return ThemeMode.system;
56 | }
57 | }
58 |
59 | @override
60 | void clearData() {
61 | sharedPrefs.remove(PrefConstants.appThemeKey);
62 | sharedPrefs.remove(PrefConstants.darkModeKey);
63 | }
64 |
65 | @override
66 | void removeKeyPair(String key) {
67 | sharedPrefs.remove(key);
68 | }
69 |
70 | @override
71 | bool keyExists(String key) {
72 | return sharedPrefs.containsKey(key);
73 | }
74 |
75 | @override
76 | bool getPrefBool(String settingsKey) {
77 | return sharedPrefs.getBool(settingsKey) ?? false;
78 | }
79 |
80 | @override
81 | int getPrefInt(String settingsKey) {
82 | return sharedPrefs.getInt(settingsKey) ?? 0;
83 | }
84 |
85 | @override
86 | String getPrefString(String settingsKey) {
87 | return sharedPrefs.getString(settingsKey) ?? '';
88 | }
89 |
90 | @override
91 | void setPrefBool(String settingsKey, bool settingsValue) {
92 | if (!settingsValue) {
93 | sharedPrefs.remove(settingsKey);
94 | return;
95 | }
96 | sharedPrefs.setBool(settingsKey, settingsValue);
97 | }
98 |
99 | @override
100 | void setPrefInt(String settingsKey, int settingsValue) {
101 | if (settingsValue.isNegative) {
102 | sharedPrefs.remove(settingsKey);
103 | return;
104 | }
105 | sharedPrefs.setInt(settingsKey, settingsValue);
106 | }
107 |
108 | @override
109 | void setPrefString(String settingsKey, String settingsValue) {
110 | if (settingsValue.isEmpty) {
111 | sharedPrefs.remove(settingsKey);
112 | return;
113 | }
114 | sharedPrefs.setString(settingsKey, settingsValue);
115 | }
116 |
117 | @override
118 | List? words = [];
119 | }
120 |
--------------------------------------------------------------------------------
/lib/presentation/widgets/inputs/form_input.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter/services.dart';
3 |
4 | import '../../theme/theme_colors.dart';
5 |
6 | class FormInput extends StatefulWidget {
7 | final String? iLabel;
8 | final TextEditingController? iController;
9 | final TextInputType iType;
10 | final AutovalidateMode? validationMode;
11 | final bool? isReadOnly;
12 | final bool? isLight;
13 | final bool? isEnabled;
14 | final bool? executeValueChange;
15 | final FormFieldValidator? iValidator;
16 | final Function(String)? onChanged;
17 | final Function()? onValueChanged;
18 | final Function()? onTap;
19 | final Widget? prefix;
20 | final bool? isActive;
21 | final bool? isMultiline;
22 | final double bdRadius;
23 | final int maxInput;
24 |
25 | const FormInput({
26 | Key? key,
27 | this.iLabel = "",
28 | this.iType = TextInputType.text,
29 | this.iController,
30 | this.validationMode = AutovalidateMode.disabled,
31 | this.isReadOnly = false,
32 | this.isLight = false,
33 | this.isEnabled = true,
34 | this.executeValueChange = false,
35 | this.iValidator,
36 | this.onChanged,
37 | this.onValueChanged,
38 | this.onTap,
39 | this.prefix,
40 | this.isActive = true,
41 | this.isMultiline = false,
42 | this.bdRadius = 5,
43 | this.maxInput = 20000,
44 | }) : super(key: key);
45 |
46 | @override
47 | FormInputState createState() => FormInputState();
48 | }
49 |
50 | class FormInputState extends State {
51 | @override
52 | Widget build(BuildContext context) {
53 | Color foreColor = widget.isLight! ? Colors.white : ThemeColors.primary;
54 |
55 | return Container(
56 | margin: const EdgeInsets.all(10),
57 | child: TextFormField(
58 | controller: widget.iController,
59 | keyboardType: widget.iType,
60 | autovalidateMode: widget.validationMode,
61 | validator: widget.iValidator,
62 | minLines: widget.isMultiline! ? 50 : 1,
63 | maxLines: widget.isMultiline! ? null : 1,
64 | enabled: widget.isEnabled,
65 | readOnly: widget.isReadOnly!,
66 | onTap: widget.onTap,
67 | inputFormatters: [
68 | LengthLimitingTextInputFormatter(widget.maxInput),
69 | ],
70 | decoration: InputDecoration(
71 | labelText: widget.iLabel,
72 | prefixIcon: widget.prefix,
73 | suffixIcon: InkWell(
74 | onTap: () => widget.iController!.clear(),
75 | child: Icon(Icons.clear, color: foreColor),
76 | ),
77 | labelStyle: TextStyle(fontSize: 16, color: foreColor),
78 | isDense: widget.isMultiline! ? true : false,
79 | contentPadding: widget.isMultiline! ? null : const EdgeInsets.all(5),
80 | enabledBorder: OutlineInputBorder(
81 | borderRadius: BorderRadius.circular(widget.bdRadius),
82 | borderSide: BorderSide(color: foreColor),
83 | ),
84 | focusedBorder: OutlineInputBorder(
85 | borderRadius: BorderRadius.circular(widget.bdRadius),
86 | borderSide: BorderSide(color: foreColor),
87 | ),
88 | ),
89 | style: TextStyle(
90 | fontSize: 18,
91 | color: foreColor,
92 | ),
93 | //textInputAction: widget.isMultiline! ? TextInputAction.newline : TextInputAction.next,
94 | onChanged: widget.onChanged,
95 | ),
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/lib/presentation/navigator/main_navigator.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | import '../widgets/text_scale_factor.dart';
4 | import '../screens/datainit/data_init_screen.dart';
5 | import '../screens/home/home_screen.dart';
6 | import '../screens/settings/settings_screen.dart';
7 | import '../screens/splash/splash_screen.dart';
8 | import 'route_names.dart';
9 |
10 | class MainNavigator extends StatefulWidget {
11 | final Widget? child;
12 |
13 | const MainNavigator({this.child, super.key});
14 |
15 | @override
16 | MainNavigatorState createState() => MainNavigatorState();
17 |
18 | static MainNavigationMixin of(BuildContext context,
19 | {bool rootNavigator = false}) {
20 | final navigator = rootNavigator
21 | ? context.findRootAncestorStateOfType()
22 | : context.findAncestorStateOfType();
23 | assert(() {
24 | if (navigator == null) {
25 | throw FlutterError(
26 | 'MainNavigation operation requested with a context that does not include a MainNavigation.\n'
27 | 'The context used to push or pop routes from the MainNavigation must be that of a '
28 | 'widget that is a descendant of a MainNavigator widget.');
29 | }
30 | return true;
31 | }());
32 | return navigator!;
33 | }
34 | }
35 |
36 | class MainNavigatorState extends State with MainNavigationMixin {
37 | static final GlobalKey _navigationKey =
38 | GlobalKey();
39 | static final List _navigatorObservers = [];
40 |
41 | static String get initialRoute => RouteNames.splash;
42 |
43 | static GlobalKey get navigationKey => _navigationKey;
44 |
45 | static List get navigatorObservers => _navigatorObservers;
46 |
47 | NavigatorState get navigator => _navigationKey.currentState!;
48 |
49 | @override
50 | Widget build(BuildContext context) {
51 | return TextScaleFactor(
52 | child: widget.child ?? const SizedBox.shrink(),
53 | );
54 | }
55 |
56 | static Route? onGenerateRoute(RouteSettings settings) {
57 | final strippedPath = settings.name?.replaceFirst('/', '');
58 | final Map routes = {
59 | '': (context) => const SplashScreen(),
60 | RouteNames.splash: (context) => const SplashScreen(),
61 | RouteNames.dataInit: (context) => const DataInitScreen(),
62 | RouteNames.home: (context) => const HomeScreen(),
63 | RouteNames.settings: (context) => const SettingsScreen(),
64 | };
65 |
66 | defaultRoute(context) => const SplashScreen();
67 |
68 | WidgetBuilder? getRouteBuilder(String routeName) {
69 | if (routes.containsKey(routeName)) {
70 | return routes[routeName];
71 | } else {
72 | return defaultRoute;
73 | }
74 | }
75 |
76 | MaterialPageRoute createMaterialPageRoute(
77 | WidgetBuilder builder, RouteSettings settings) {
78 | return MaterialPageRoute(
79 | builder: builder,
80 | settings: settings,
81 | );
82 | }
83 |
84 | WidgetBuilder? routeBuilder = getRouteBuilder(strippedPath!);
85 | if (routeBuilder != null) {
86 | return createMaterialPageRoute(routeBuilder, settings);
87 | } else {
88 | return null;
89 | }
90 | }
91 | }
92 |
93 | abstract class MainNavigation {}
94 |
95 | mixin MainNavigationMixin on State
96 | implements MainNavigation {}
97 |
--------------------------------------------------------------------------------
/lib/presentation/screens/datainit/data_init_screen.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_bloc/flutter_bloc.dart';
3 | import 'package:percent_indicator/percent_indicator.dart';
4 |
5 | import '../../widgets/progress/custom_snackbar.dart';
6 | import '../../widgets/progress/general_progress.dart';
7 | import '../../navigator/route_names.dart';
8 | import '../../theme/theme_colors.dart';
9 | import '../../blocs/datainit/data_init_bloc.dart';
10 |
11 | class DataInitScreen extends StatefulWidget {
12 | const DataInitScreen({super.key});
13 |
14 | @override
15 | State createState() => DataInitScreenState();
16 | }
17 |
18 | class DataInitScreenState extends State {
19 | @override
20 | Widget build(BuildContext context) {
21 | return BlocProvider(
22 | create: (context) => DataInitBloc()..add(const FetchData()),
23 | child: BlocConsumer(
24 | listener: (context, state) {
25 | if (state is DataInitFetchedState) {
26 | context.read().add(
27 | SaveData(
28 | state.idioms,
29 | state.proverbs,
30 | state.sayings,
31 | state.words,
32 | ),
33 | );
34 | } else if (state is DataInitFailureState) {
35 | CustomSnackbar.show(context, state.feedback);
36 | } else if (state is DataInitSavedState) {
37 | Navigator.pushNamedAndRemoveUntil(
38 | context,
39 | RouteNames.home,
40 | (route) => false,
41 | );
42 | }
43 | },
44 | builder: (context, state) {
45 | var bloc = context.read();
46 | return Scaffold(
47 | body: state.maybeWhen(
48 | orElse: () => const SizedBox(),
49 | failure: (feedback) => EmptyState(
50 | title: 'Hamna chochote hapa',
51 | showRetry: true,
52 | onRetry: () => bloc.add(const FetchData()),
53 | ),
54 | progress: () => const LoadingProgress(title: "Inapakia data ..."),
55 | saving: (feedback, progress) => Column(
56 | children: [
57 | const Spacer(),
58 | LoadingProgress(title: feedback),
59 | if (progress > 0.0) ...[
60 | Container(
61 | height: 50,
62 | padding: const EdgeInsets.symmetric(horizontal: 20),
63 | child: LinearPercentIndicator(
64 | lineHeight: 50,
65 | percent: progress * .01,
66 | center: Text(
67 | "$progress %",
68 | style: const TextStyle(
69 | fontSize: 40,
70 | fontWeight: FontWeight.bold,
71 | ),
72 | ),
73 | barRadius: const Radius.circular(20),
74 | backgroundColor: ThemeColors.bgColorPrimary4(context),
75 | progressColor: Colors.green,
76 | ),
77 | ),
78 | ],
79 | const Spacer(),
80 | ],
81 | ),
82 | ),
83 | );
84 | },
85 | ),
86 | );
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/presentation/screens/home/widgets/lists/proverbs_list.dart:
--------------------------------------------------------------------------------
1 | part of '../../home_screen.dart';
2 |
3 | class ProverbsList extends StatelessWidget {
4 | final HomeScreenState parent;
5 | const ProverbsList({super.key, required this.parent});
6 |
7 | @override
8 | Widget build(BuildContext context) {
9 | final Size size = MediaQuery.of(context).size;
10 |
11 | var emptyState = const EmptyState(title: 'Samahani hamna chochote hapa');
12 |
13 | var listView = ListView.builder(
14 | padding: const EdgeInsets.only(right: 15),
15 | itemCount: parent.filteredProverbs.length,
16 | itemBuilder: (BuildContext context, int index) {
17 | final Proverb proverb = parent.filteredProverbs[index];
18 | return ProverbItem(
19 | proverb: proverb,
20 | onTap: () {}, //=> vm.openProverb(proverb),
21 | );
22 | },
23 | );
24 |
25 | return SizedBox(
26 | width: size.width - 85,
27 | child: parent.filteredProverbs.isEmpty ? emptyState : listView,
28 | );
29 | }
30 | }
31 |
32 | class ProverbItem extends StatelessWidget {
33 | final Proverb proverb;
34 | final VoidCallback? onTap;
35 | const ProverbItem({super.key, required this.proverb, this.onTap});
36 |
37 | @override
38 | Widget build(BuildContext context) {
39 | final titleTxtStyle =
40 | TextStyles.headingStyle4.bold.size(22).textHeight(1.2);
41 | final bodyTxtStyle = TextStyles.bodyStyle1.size(18);
42 |
43 | var meaning = cleanMeaning(proverb.meaning ?? "");
44 |
45 | final contents = meaning.split("|");
46 | var extra = contents.isNotEmpty ? contents[0].split(":") : [];
47 | meaning = extra.isNotEmpty ? " ~ ${extra[0].trim()}." : "";
48 |
49 | if (contents.length > 1) {
50 | extra = contents[1].split(":");
51 | meaning = "$meaning\n ~ ${extra[0].trim()}.";
52 | }
53 |
54 | final synonyms =
55 | (proverb.synonyms ?? "").split(',').where((s) => s.isNotEmpty).toList();
56 | final synonymsContainer = synonyms.isNotEmpty
57 | ? Row(
58 | children: [
59 | Text(
60 | "${synonyms.length == 1 ? 'KISAWE' : 'VISAWE ${synonyms.length}'}:",
61 | style: bodyTxtStyle.bold,
62 | ),
63 | const SizedBox(width: 10),
64 | Flexible(
65 | child: SizedBox(
66 | height: 35,
67 | child: ListView.builder(
68 | scrollDirection: Axis.horizontal,
69 | itemCount: synonyms.length,
70 | itemBuilder: (BuildContext context, int index) {
71 | return TagView(tagText: synonyms[index], size: 20);
72 | },
73 | ),
74 | ),
75 | ),
76 | ],
77 | )
78 | : const SizedBox.shrink();
79 |
80 | return Card(
81 | elevation: 2,
82 | child: ListTile(
83 | onTap: onTap,
84 | title: Text(proverb.title ?? "", style: titleTxtStyle),
85 | subtitle: Column(
86 | crossAxisAlignment: CrossAxisAlignment.start,
87 | children: [
88 | if (meaning.isNotEmpty)
89 | Text(meaning, style: bodyTxtStyle, maxLines: 2),
90 | if (synonyms.isNotEmpty) const SizedBox(height: 5),
91 | if (synonyms.isNotEmpty) synonymsContainer,
92 | const SizedBox(height: 10),
93 | ],
94 | ),
95 | ),
96 | );
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: swahilib
2 | description: Kiswahili Kitukuzwe - Kamusi ya Kiswahili ya maneno, nahau, methali na misemo
3 | publish_to: 'none'
4 |
5 | version: 1.0.125
6 |
7 | environment:
8 | sdk: ^3.5.4
9 |
10 | dependencies:
11 | connectivity_plus: ^6.1.3 # Check network connectivity status (WiFi/Mobile/Offline)
12 | cupertino_icons: ^1.0.8 # iOS-style icons for use with the Cupertino widgets
13 | dartx: ^1.2.0 # Kotlin-like extension methods for Dart collections and primitives
14 | floor: ^1.5.0 # SQLite abstraction for Flutter with DAO pattern
15 | flutter:
16 | sdk: flutter # Core Flutter SDK
17 | flutter_bloc: ^8.1.6 # State management library based on BLoC (Business Logic Component) pattern
18 | flutter_html: ^3.0.0-beta.2 # Render HTML content as Flutter widgets
19 | freezed_annotation: ^2.4.4 # Annotations for generating immutable classes with union/pattern matching
20 | get_it: ^8.0.2 # Simple service locator for dependency injection
21 | http: ^1.3.0 # HTTP client for making web requests
22 | injectable: ^2.5.0 # Dependency injection framework for Flutter using annotations
23 | intl: ^0.19.0 # Internationalization and localization support
24 | json_annotation: ^4.9.0 # Annotations for code generation to serialize/deserialize JSON
25 | loading_indicator: ^3.1.1 # Various loading animations for async operations
26 | package_info_plus: ^8.1.1 # Access app version, package name, and build number
27 | path: ^1.9.0 # Path manipulation utilities (for files, directories, etc.)
28 | path_provider: ^2.1.5 # Access commonly used locations on the filesystem
29 | percent_indicator: ^4.2.4 # Circular and linear percent indicators
30 | rxdart: ^0.28.0 # Reactive functional programming with observables and streams
31 | sentry_flutter: ^8.5.0 # Error tracking and performance monitoring using Sentry
32 | share_plus: ^10.1.2 # Share content from your Flutter app with other apps
33 | shared_preferences: ^2.3.3 # Persistent storage for simple key-value pairs
34 | sizer: ^3.0.4 # Helps with responsive UI using screen dimensions
35 | skeleton_loader: ^2.0.0+4 # Display shimmer loading placeholders for content
36 | sqflite: ^2.4.1 # SQLite plugin for Flutter
37 | styled_widget: ^0.4.1 # Simplifies Flutter widget styling through extension methods
38 | supabase_flutter: ^2.8.3 # Supabase client for Flutter, provides auth, storage, database access
39 | textstyle_extensions: ^2.0.0-nullsafety # Utility extensions for `TextStyle` manipulation
40 | url_launcher: ^6.3.1 # Launch URLs (email, phone, web) in a mobile browser or other apps
41 |
42 | dev_dependencies:
43 | build_runner: ^2.4.13 # Code generation tool (used with freezed, json_serializable, etc.)
44 | floor_generator: ^1.5.0 # Generates the database access code for Floor
45 | flutter_lints: ^4.0.0 # Recommended lint rules for Flutter apps
46 | flutter_test:
47 | sdk: flutter # Flutter's built-in testing framework
48 | freezed: ^2.5.7 # Code generator for creating immutable classes with union types
49 | injectable_generator: ^2.6.2 # Code generator for injectable DI setup
50 | json_serializable: ^6.8.0 # Code generator for serializing classes to/from JSON
51 |
52 |
53 | flutter:
54 | generate: true
55 | uses-material-design: true
56 | assets:
57 | - assets/fonts/
58 | - assets/icons/
59 | - assets/images/
60 | - assets/sound/
61 | fonts:
62 | - family: TrebuchetMS
63 | fonts:
64 | - asset: assets/fonts/Trebuchet-MS.ttf
65 |
--------------------------------------------------------------------------------
/windows/runner/Runner.rc:
--------------------------------------------------------------------------------
1 | // Microsoft Visual C++ generated resource script.
2 | //
3 | #pragma code_page(65001)
4 | #include "resource.h"
5 |
6 | #define APSTUDIO_READONLY_SYMBOLS
7 | /////////////////////////////////////////////////////////////////////////////
8 | //
9 | // Generated from the TEXTINCLUDE 2 resource.
10 | //
11 | #include "winres.h"
12 |
13 | /////////////////////////////////////////////////////////////////////////////
14 | #undef APSTUDIO_READONLY_SYMBOLS
15 |
16 | /////////////////////////////////////////////////////////////////////////////
17 | // English (United States) resources
18 |
19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)
20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
21 |
22 | #ifdef APSTUDIO_INVOKED
23 | /////////////////////////////////////////////////////////////////////////////
24 | //
25 | // TEXTINCLUDE
26 | //
27 |
28 | 1 TEXTINCLUDE
29 | BEGIN
30 | "resource.h\0"
31 | END
32 |
33 | 2 TEXTINCLUDE
34 | BEGIN
35 | "#include ""winres.h""\r\n"
36 | "\0"
37 | END
38 |
39 | 3 TEXTINCLUDE
40 | BEGIN
41 | "\r\n"
42 | "\0"
43 | END
44 |
45 | #endif // APSTUDIO_INVOKED
46 |
47 |
48 | /////////////////////////////////////////////////////////////////////////////
49 | //
50 | // Icon
51 | //
52 |
53 | // Icon with lowest ID value placed first to ensure application icon
54 | // remains consistent on all systems.
55 | IDI_APP_ICON ICON "resources\\app_icon.ico"
56 |
57 |
58 | /////////////////////////////////////////////////////////////////////////////
59 | //
60 | // Version
61 | //
62 |
63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD)
64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD
65 | #else
66 | #define VERSION_AS_NUMBER 1,0,0,0
67 | #endif
68 |
69 | #if defined(FLUTTER_VERSION)
70 | #define VERSION_AS_STRING FLUTTER_VERSION
71 | #else
72 | #define VERSION_AS_STRING "1.0.0"
73 | #endif
74 |
75 | VS_VERSION_INFO VERSIONINFO
76 | FILEVERSION VERSION_AS_NUMBER
77 | PRODUCTVERSION VERSION_AS_NUMBER
78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
79 | #ifdef _DEBUG
80 | FILEFLAGS VS_FF_DEBUG
81 | #else
82 | FILEFLAGS 0x0L
83 | #endif
84 | FILEOS VOS__WINDOWS32
85 | FILETYPE VFT_APP
86 | FILESUBTYPE 0x0L
87 | BEGIN
88 | BLOCK "StringFileInfo"
89 | BEGIN
90 | BLOCK "040904e4"
91 | BEGIN
92 | VALUE "CompanyName", "Futuristic Ke" "\0"
93 | VALUE "FileDescription", "SwahiLib" "\0"
94 | VALUE "FileVersion", VERSION_AS_STRING "\0"
95 | VALUE "InternalName", "SwahiLib" "\0"
96 | VALUE "LegalCopyright", "Copyright (C) 2024 Futuristic Ke. All rights reserved." "\0"
97 | VALUE "OriginalFilename", "SwahiLib.exe" "\0"
98 | VALUE "ProductName", "SwahiLib" "\0"
99 | VALUE "ProductVersion", VERSION_AS_STRING "\0"
100 | END
101 | END
102 | BLOCK "VarFileInfo"
103 | BEGIN
104 | VALUE "Translation", 0x409, 1252
105 | END
106 | END
107 |
108 | #endif // English (United States) resources
109 | /////////////////////////////////////////////////////////////////////////////
110 |
111 |
112 |
113 | #ifndef APSTUDIO_INVOKED
114 | /////////////////////////////////////////////////////////////////////////////
115 | //
116 | // Generated from the TEXTINCLUDE 3 resource.
117 | //
118 |
119 |
120 | /////////////////////////////////////////////////////////////////////////////
121 | #endif // not APSTUDIO_INVOKED
122 |
--------------------------------------------------------------------------------