From 8a562e5bf65b69f35e5bc8a6348430c4a515e7e3 Mon Sep 17 00:00:00 2001 From: y1lm0z Date: Tue, 14 Jul 2026 17:37:17 +0300 Subject: [PATCH] libretranslatr --- .gitignore | 133 ++----- .metadata | 33 ++ README.md | 18 +- analysis_options.yaml | 28 ++ android/.gitignore | 14 + android/app/build.gradle.kts | 45 +++ android/app/src/debug/AndroidManifest.xml | 7 + android/app/src/main/AndroidManifest.xml | 45 +++ .../com/y1lm0z/libretranslate/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + android/app/src/main/res/values/styles.xml | 18 + android/app/src/profile/AndroidManifest.xml | 7 + android/build.gradle.kts | 24 ++ android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + android/settings.gradle.kts | 26 ++ lib/main.dart | 23 ++ lib/models/history_item.dart | 31 ++ lib/models/language.dart | 29 ++ lib/screens/history_screen.dart | 117 ++++++ lib/screens/home_screen.dart | 368 ++++++++++++++++++ lib/screens/settings_screen.dart | 164 ++++++++ lib/services/history_service.dart | 33 ++ lib/services/settings_service.dart | 31 ++ lib/services/translate_service.dart | 125 ++++++ lib/theme/app_theme.dart | 37 ++ linux/.gitignore | 1 + linux/CMakeLists.txt | 128 ++++++ linux/flutter/CMakeLists.txt | 88 +++++ linux/flutter/generated_plugin_registrant.cc | 11 + linux/flutter/generated_plugin_registrant.h | 15 + linux/flutter/generated_plugins.cmake | 23 ++ linux/runner/CMakeLists.txt | 26 ++ linux/runner/main.cc | 6 + linux/runner/my_application.cc | 148 +++++++ linux/runner/my_application.h | 21 + pubspec.lock | 213 ++++++++++ pubspec.yaml | 22 ++ test/widget_test.dart | 30 ++ 46 files changed, 2040 insertions(+), 106 deletions(-) create mode 100644 .metadata create mode 100644 analysis_options.yaml create mode 100644 android/.gitignore create mode 100644 android/app/build.gradle.kts create mode 100644 android/app/src/debug/AndroidManifest.xml create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/kotlin/com/y1lm0z/libretranslate/MainActivity.kt create mode 100644 android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 android/app/src/main/res/drawable/launch_background.xml create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values-night/styles.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/app/src/profile/AndroidManifest.xml create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100644 android/settings.gradle.kts create mode 100644 lib/main.dart create mode 100644 lib/models/history_item.dart create mode 100644 lib/models/language.dart create mode 100644 lib/screens/history_screen.dart create mode 100644 lib/screens/home_screen.dart create mode 100644 lib/screens/settings_screen.dart create mode 100644 lib/services/history_service.dart create mode 100644 lib/services/settings_service.dart create mode 100644 lib/services/translate_service.dart create mode 100644 lib/theme/app_theme.dart create mode 100644 linux/.gitignore create mode 100644 linux/CMakeLists.txt create mode 100644 linux/flutter/CMakeLists.txt create mode 100644 linux/flutter/generated_plugin_registrant.cc create mode 100644 linux/flutter/generated_plugin_registrant.h create mode 100644 linux/flutter/generated_plugins.cmake create mode 100644 linux/runner/CMakeLists.txt create mode 100644 linux/runner/main.cc create mode 100644 linux/runner/my_application.cc create mode 100644 linux/runner/my_application.h create mode 100644 pubspec.lock create mode 100644 pubspec.yaml create mode 100644 test/widget_test.dart diff --git a/.gitignore b/.gitignore index 8b26114..3820a95 100644 --- a/.gitignore +++ b/.gitignore @@ -1,120 +1,45 @@ -# ---> Flutter # Miscellaneous *.class -*.lock *.log *.pyc *.swp +.DS_Store +.atom/ +.build/ .buildlog/ .history +.svn/ +.swiftpm/ +migrate_working_dir/ +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ - -# Flutter repo-specific -/bin/cache/ -/bin/internal/bootstrap.bat -/bin/internal/bootstrap.sh -/bin/mingit/ -/dev/benchmarks/mega_gallery/ -/dev/bots/.recipe_deps -/dev/bots/android_tools/ -/dev/devicelab/ABresults*.json -/dev/docs/doc/ -/dev/docs/flutter.docs.zip -/dev/docs/lib/ -/dev/docs/pubspec.yaml -/dev/integration_tests/**/xcuserdata -/dev/integration_tests/**/Pods -/packages/flutter/coverage/ -version -analysis_benchmark.json - -# packages file containing multi-root paths -.packages.generated +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ # Flutter/Dart/Pub related **/doc/api/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -**/generated_plugin_registrant.dart -.packages -.pub-preload-cache/ -.pub/ -build/ -flutter_*.png -linked_*.ds -unlinked.ds -unlinked_spec.ds - -# Android related -**/android/**/gradle-wrapper.jar -.gradle/ -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java -**/android/key.properties -*.jks - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ **/ios/Flutter/.last_build_id -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/ephemeral -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ -# macOS -**/Flutter/ephemeral/ -**/Pods/ -**/macos/Flutter/GeneratedPluginRegistrant.swift -**/macos/Flutter/ephemeral -**/xcuserdata/ - -# Windows -**/windows/flutter/generated_plugin_registrant.cc -**/windows/flutter/generated_plugin_registrant.h -**/windows/flutter/generated_plugins.cmake - -# Linux -**/linux/flutter/generated_plugin_registrant.cc -**/linux/flutter/generated_plugin_registrant.h -**/linux/flutter/generated_plugins.cmake - -# Coverage -coverage/ - -# Symbols +# Symbolication related app.*.symbols -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages -!/dev/ci/**/Gemfile.lock +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..dd4a886 --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ee80f08bbf97172ec030b8751ceab557177a34a6" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + - platform: android + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + - platform: linux + create_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + base_revision: ee80f08bbf97172ec030b8751ceab557177a34a6 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/README.md b/README.md index ce19dbf..5a81a2d 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,17 @@ -# Libretranslate-app +# libretranslate -Libretranslate için android ve linux uygulaması \ No newline at end of file +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..18fc8f8 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.y1lm0z.libretranslate" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.y1lm0z.libretranslate" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0a357fb --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/y1lm0z/libretranslate/MainActivity.kt b/android/app/src/main/kotlin/com/y1lm0z/libretranslate/MainActivity.kt new file mode 100644 index 0000000..7103b18 --- /dev/null +++ b/android/app/src/main/kotlin/com/y1lm0z/libretranslate/MainActivity.kt @@ -0,0 +1,5 @@ +package com.y1lm0z.libretranslate + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..86b4df1 --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'screens/home_screen.dart'; +import 'theme/app_theme.dart'; + +void main() { + runApp(const TranslateApp()); +} + +class TranslateApp extends StatelessWidget { + const TranslateApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Çeviri', + debugShowCheckedModeBanner: false, + theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: ThemeMode.system, + home: const HomeScreen(), + ); + } +} diff --git a/lib/models/history_item.dart b/lib/models/history_item.dart new file mode 100644 index 0000000..7a1a255 --- /dev/null +++ b/lib/models/history_item.dart @@ -0,0 +1,31 @@ +class HistoryItem { + final String sourceLang; + final String targetLang; + final String sourceText; + final String translatedText; + final DateTime timestamp; + + HistoryItem({ + required this.sourceLang, + required this.targetLang, + required this.sourceText, + required this.translatedText, + required this.timestamp, + }); + + Map toJson() => { + 'sourceLang': sourceLang, + 'targetLang': targetLang, + 'sourceText': sourceText, + 'translatedText': translatedText, + 'timestamp': timestamp.toIso8601String(), + }; + + factory HistoryItem.fromJson(Map json) => HistoryItem( + sourceLang: json['sourceLang'] as String, + targetLang: json['targetLang'] as String, + sourceText: json['sourceText'] as String, + translatedText: json['translatedText'] as String, + timestamp: DateTime.parse(json['timestamp'] as String), + ); +} diff --git a/lib/models/language.dart b/lib/models/language.dart new file mode 100644 index 0000000..cdb9ffb --- /dev/null +++ b/lib/models/language.dart @@ -0,0 +1,29 @@ +class Language { + final String code; + final String name; + final List targets; + + const Language({ + required this.code, + required this.name, + this.targets = const [], + }); + + factory Language.fromJson(Map json) { + return Language( + code: json['code'] as String, + name: json['name'] as String, + targets: (json['targets'] as List?)?.map((e) => e.toString()).toList() ?? + const [], + ); + } + + /// "Otomatik Algıla" seçeneğini temsil eden özel dil. + static const auto = Language(code: 'auto', name: 'Otomatik Algıla'); + + @override + bool operator ==(Object other) => other is Language && other.code == code; + + @override + int get hashCode => code.hashCode; +} diff --git a/lib/screens/history_screen.dart b/lib/screens/history_screen.dart new file mode 100644 index 0000000..0ed528e --- /dev/null +++ b/lib/screens/history_screen.dart @@ -0,0 +1,117 @@ +import 'package:flutter/material.dart'; +import '../models/history_item.dart'; +import '../services/history_service.dart'; + +class HistoryScreen extends StatefulWidget { + const HistoryScreen({super.key}); + + @override + State createState() => _HistoryScreenState(); +} + +class _HistoryScreenState extends State { + final _historyService = HistoryService(); + List _items = []; + bool _loading = true; + + @override + void initState() { + super.initState(); + _load(); + } + + Future _load() async { + final items = await _historyService.getAll(); + if (!mounted) return; + setState(() { + _items = items; + _loading = false; + }); + } + + Future _clear() async { + final confirmed = await showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Geçmişi Sil'), + content: const Text('Tüm çeviri geçmişi silinsin mi? Bu işlem geri alınamaz.'), + actions: [ + TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('Vazgeç')), + FilledButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('Sil')), + ], + ), + ); + if (confirmed == true) { + await _historyService.clear(); + _load(); + } + } + + String _formatTime(DateTime dt) { + final now = DateTime.now(); + final sameDay = dt.year == now.year && dt.month == now.month && dt.day == now.day; + final time = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; + if (sameDay) return time; + return '${dt.day}.${dt.month}.${dt.year} $time'; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Geçmiş'), + actions: [ + if (_items.isNotEmpty) + IconButton( + icon: const Icon(Icons.delete_outline), + tooltip: 'Geçmişi temizle', + onPressed: _clear, + ), + ], + ), + body: _loading + ? const Center(child: CircularProgressIndicator()) + : _items.isEmpty + ? const Center(child: Text('Henüz çeviri geçmişi yok')) + : ListView.separated( + padding: const EdgeInsets.all(12), + itemCount: _items.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (ctx, i) { + final item = _items[i]; + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + '${item.sourceLang.toUpperCase()} → ${item.targetLang.toUpperCase()}', + style: Theme.of(context).textTheme.labelMedium, + ), + const Spacer(), + Text( + _formatTime(item.timestamp), + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + const SizedBox(height: 6), + Text(item.sourceText), + const Divider(height: 16), + Text( + item.translatedText, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart new file mode 100644 index 0000000..e43d9d4 --- /dev/null +++ b/lib/screens/home_screen.dart @@ -0,0 +1,368 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import '../models/language.dart'; +import '../models/history_item.dart'; +import '../services/settings_service.dart'; +import '../services/translate_service.dart'; +import '../services/history_service.dart'; +import 'settings_screen.dart'; +import 'history_screen.dart'; + +class HomeScreen extends StatefulWidget { + const HomeScreen({super.key}); + + @override + State createState() => _HomeScreenState(); +} + +class _HomeScreenState extends State { + final _settingsService = SettingsService(); + late final TranslateService _translateService = TranslateService(_settingsService); + final _historyService = HistoryService(); + + final _sourceController = TextEditingController(); + Timer? _debounce; + + List _languages = []; + Language _sourceLang = Language.auto; + Language _targetLang = const Language(code: 'en', name: 'İngilizce'); + + String _resultText = ''; + String? _detectedLangLabel; + bool _loadingLanguages = true; + bool _translating = false; + String? _connectionError; + + @override + void initState() { + super.initState(); + _bootstrap(); + _sourceController.addListener(_onTextChanged); + } + + @override + void dispose() { + _debounce?.cancel(); + _sourceController.dispose(); + super.dispose(); + } + + /// Uygulama açılışında sunucuya otomatik bağlanmayı dener. + Future _bootstrap() async { + final hasSettings = await _settingsService.hasSettings(); + if (!hasSettings) { + final saved = await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen(isFirstRun: true)), + ); + if (saved != true) { + // Kullanıcı ayarları kaydetmeden geri döndüyse yine de tekrar dene. + } + } + await _loadLanguages(); + } + + Future _loadLanguages() async { + setState(() { + _loadingLanguages = true; + _connectionError = null; + }); + try { + final langs = await _translateService.fetchLanguages(); + setState(() { + _languages = langs; + // Hedef dil listede yoksa ilk uygun dile düş. + if (!_languages.any((l) => l.code == _targetLang.code) && _languages.isNotEmpty) { + _targetLang = _languages.firstWhere((l) => l.code != 'auto', orElse: () => _languages.first); + } + }); + } catch (e) { + setState(() => _connectionError = e.toString()); + } finally { + setState(() => _loadingLanguages = false); + } + } + + void _onTextChanged() { + _debounce?.cancel(); + _debounce = Timer(const Duration(milliseconds: 600), _runTranslate); + } + + Future _runTranslate() async { + final text = _sourceController.text; + if (text.trim().isEmpty) { + setState(() { + _resultText = ''; + _detectedLangLabel = null; + }); + return; + } + + setState(() => _translating = true); + try { + String effectiveSource = _sourceLang.code; + + if (_sourceLang.code == 'auto') { + final detected = await _translateService.detect(text); + if (detected != null) { + effectiveSource = detected.language; + final match = _languages.firstWhere( + (l) => l.code == detected.language, + orElse: () => Language(code: detected.language, name: detected.language), + ); + setState(() => _detectedLangLabel = 'Algılanan dil: ${match.name}'); + } + } else { + _detectedLangLabel = null; + } + + final translated = await _translateService.translate( + text: text, + sourceLang: effectiveSource, + targetLang: _targetLang.code, + ); + + if (!mounted) return; + setState(() => _resultText = translated); + + await _historyService.add(HistoryItem( + sourceLang: effectiveSource, + targetLang: _targetLang.code, + sourceText: text, + translatedText: translated, + timestamp: DateTime.now(), + )); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('Çeviri hatası: $e')), + ); + } finally { + if (mounted) setState(() => _translating = false); + } + } + + void _swapLanguages() { + if (_sourceLang.code == 'auto') return; // otomatik iken takas anlamsız + setState(() { + final tmp = _sourceLang; + _sourceLang = _targetLang; + _targetLang = tmp; + final tmpText = _sourceController.text; + _sourceController.text = _resultText; + _resultText = tmpText; + }); + } + + List get _sourceOptions => [Language.auto, ..._languages]; + List get _targetOptions => _languages; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Çeviri'), + actions: [ + IconButton( + tooltip: 'Geçmiş', + icon: const Icon(Icons.history), + onPressed: () => Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const HistoryScreen()), + ), + ), + IconButton( + tooltip: 'Ayarlar', + icon: const Icon(Icons.settings_outlined), + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen()), + ); + _loadLanguages(); + }, + ), + ], + ), + body: _loadingLanguages + ? const Center(child: CircularProgressIndicator()) + : _connectionError != null + ? _buildConnectionError() + : _buildTranslator(), + ); + } + + Widget _buildConnectionError() { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.cloud_off, size: 48, color: Colors.grey), + const SizedBox(height: 16), + Text('Sunucuya bağlanılamadı', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 8), + Text(_connectionError!, textAlign: TextAlign.center), + const SizedBox(height: 20), + Wrap( + spacing: 12, + children: [ + OutlinedButton(onPressed: _loadLanguages, child: const Text('Tekrar Dene')), + FilledButton( + onPressed: () async { + await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen()), + ); + _loadLanguages(); + }, + child: const Text('Ayarları Aç'), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildTranslator() { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _buildLanguageBar(), + const SizedBox(height: 12), + Expanded( + child: Column( + children: [ + Expanded(child: _buildInputCard()), + const SizedBox(height: 12), + Expanded(child: _buildResultCard()), + ], + ), + ), + ], + ), + ); + } + + Widget _buildLanguageBar() { + return Row( + children: [ + Expanded(child: _buildLangDropdown(isSource: true)), + IconButton( + icon: const Icon(Icons.swap_horiz), + tooltip: 'Dilleri değiştir', + onPressed: _sourceLang.code == 'auto' ? null : _swapLanguages, + ), + Expanded(child: _buildLangDropdown(isSource: false)), + ], + ); + } + + Widget _buildLangDropdown({required bool isSource}) { + final options = isSource ? _sourceOptions : _targetOptions; + final value = isSource ? _sourceLang : _targetLang; + return DropdownButtonFormField( + isExpanded: true, + value: options.contains(value) ? value : null, + decoration: const InputDecoration(isDense: true), + items: options + .map((l) => DropdownMenuItem(value: l, child: Text(l.name, overflow: TextOverflow.ellipsis))) + .toList(), + onChanged: (l) { + if (l == null) return; + setState(() { + if (isSource) { + _sourceLang = l; + } else { + _targetLang = l; + } + }); + _runTranslate(); + }, + ); + } + + Widget _buildInputCard() { + return Card( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: TextField( + controller: _sourceController, + maxLines: null, + expands: true, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'Çevrilecek metni yazın...', + ), + ), + ), + if (_detectedLangLabel != null) + Padding( + padding: const EdgeInsets.only(top: 4), + child: Text(_detectedLangLabel!, style: Theme.of(context).textTheme.bodySmall), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: const Icon(Icons.clear, size: 20), + tooltip: 'Temizle', + onPressed: () => _sourceController.clear(), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildResultCard() { + return Card( + margin: EdgeInsets.zero, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: _translating + ? const Center(child: CircularProgressIndicator()) + : SingleChildScrollView( + child: SelectableText( + _resultText, + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + IconButton( + icon: const Icon(Icons.copy, size: 20), + tooltip: 'Kopyala', + onPressed: _resultText.isEmpty + ? null + : () { + Clipboard.setData(ClipboardData(text: _resultText)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Kopyalandı'), duration: Duration(seconds: 1)), + ); + }, + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart new file mode 100644 index 0000000..4bd0c78 --- /dev/null +++ b/lib/screens/settings_screen.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import '../services/settings_service.dart'; +import '../services/translate_service.dart'; + +class SettingsScreen extends StatefulWidget { + final bool isFirstRun; + const SettingsScreen({super.key, this.isFirstRun = false}); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + final _settings = SettingsService(); + final _urlController = TextEditingController(); + final _apiKeyController = TextEditingController(); + + bool _testing = false; + String? _statusMessage; + bool? _statusSuccess; + + @override + void initState() { + super.initState(); + _load(); + _urlController.addListener(() => setState(() {})); + } + + Future _load() async { + final url = await _settings.getServerUrl(); + final key = await _settings.getApiKey(); + if (!mounted) return; + setState(() { + _urlController.text = url ?? ''; + _apiKeyController.text = key ?? ''; + }); + } + + Future _testConnection() async { + setState(() { + _testing = true; + _statusMessage = null; + }); + + // Test için önce girilen değerleri geçici olarak kaydediyoruz. + await _settings.save( + serverUrl: _urlController.text, + apiKey: _apiKeyController.text, + ); + + try { + final service = TranslateService(_settings); + final langs = await service.fetchLanguages(); + setState(() { + _statusSuccess = true; + _statusMessage = 'Bağlantı başarılı — ${langs.length} dil bulundu.'; + }); + } catch (e) { + setState(() { + _statusSuccess = false; + _statusMessage = 'Bağlanamadı: $e'; + }); + } finally { + setState(() => _testing = false); + } + } + + Future _saveAndReturn() async { + await _settings.save( + serverUrl: _urlController.text, + apiKey: _apiKeyController.text, + ); + if (!mounted) return; + Navigator.of(context).pop(true); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Sunucu Ayarları'), + automaticallyImplyLeading: !widget.isFirstRun, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.isFirstRun) ...[ + Text( + 'LibreTranslate Sunucunuza Bağlanın', + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 8), + Text( + 'Uygulamanın çeviri yapabilmesi için kendi LibreTranslate sunucunuzun adresini girin.', + style: Theme.of(context).textTheme.bodyMedium, + ), + const SizedBox(height: 24), + ], + TextField( + controller: _urlController, + keyboardType: TextInputType.url, + decoration: const InputDecoration( + labelText: 'Sunucu Adresi', + hintText: 'https://translate.benimsunucum.com', + prefixIcon: Icon(Icons.dns_outlined), + ), + ), + const SizedBox(height: 14), + TextField( + controller: _apiKeyController, + obscureText: true, + decoration: const InputDecoration( + labelText: 'API Key (opsiyonel)', + hintText: 'Boş bırakılırsa gönderilmez', + prefixIcon: Icon(Icons.key_outlined), + ), + ), + const SizedBox(height: 20), + OutlinedButton.icon( + onPressed: _testing ? null : _testConnection, + icon: _testing + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.wifi_tethering), + label: Text(_testing ? 'Test ediliyor...' : 'Bağlantıyı Test Et'), + ), + if (_statusMessage != null) ...[ + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: (_statusSuccess == true + ? Colors.green + : Colors.red) + .withOpacity(0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Text( + _statusMessage!, + style: TextStyle( + color: _statusSuccess == true + ? Colors.green.shade700 + : Colors.red.shade700, + ), + ), + ), + ], + const SizedBox(height: 24), + FilledButton.icon( + onPressed: _urlController.text.trim().isEmpty ? null : _saveAndReturn, + icon: const Icon(Icons.check), + label: const Text('Kaydet'), + ), + ], + ), + ), + ); + } +} diff --git a/lib/services/history_service.dart b/lib/services/history_service.dart new file mode 100644 index 0000000..fecef1a --- /dev/null +++ b/lib/services/history_service.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../models/history_item.dart'; + +class HistoryService { + static const _key = 'translation_history'; + static const _maxItems = 200; + + Future> getAll() async { + final prefs = await SharedPreferences.getInstance(); + final raw = prefs.getStringList(_key) ?? []; + return raw + .map((s) => HistoryItem.fromJson(jsonDecode(s) as Map)) + .toList() + .reversed + .toList(); // en yeni en üstte + } + + Future add(HistoryItem item) async { + final prefs = await SharedPreferences.getInstance(); + final raw = prefs.getStringList(_key) ?? []; + raw.add(jsonEncode(item.toJson())); + final trimmed = raw.length > _maxItems + ? raw.sublist(raw.length - _maxItems) + : raw; + await prefs.setStringList(_key, trimmed); + } + + Future clear() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.remove(_key); + } +} diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart new file mode 100644 index 0000000..c95d830 --- /dev/null +++ b/lib/services/settings_service.dart @@ -0,0 +1,31 @@ +import 'package:shared_preferences/shared_preferences.dart'; + +/// Sunucu URL'si ve API key gibi ayarları kalıcı olarak saklar. +class SettingsService { + static const _keyServerUrl = 'server_url'; + static const _keyApiKey = 'api_key'; + + Future getServerUrl() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString(_keyServerUrl); + } + + Future getApiKey() async { + final prefs = await SharedPreferences.getInstance(); + final key = prefs.getString(_keyApiKey); + return (key == null || key.isEmpty) ? null : key; + } + + Future save({required String serverUrl, required String apiKey}) async { + final prefs = await SharedPreferences.getInstance(); + // Sonundaki "/" karakterini temizle, karışıklık olmasın. + final cleanUrl = serverUrl.trim().replaceAll(RegExp(r'/+$'), ''); + await prefs.setString(_keyServerUrl, cleanUrl); + await prefs.setString(_keyApiKey, apiKey.trim()); + } + + Future hasSettings() async { + final url = await getServerUrl(); + return url != null && url.isNotEmpty; + } +} diff --git a/lib/services/translate_service.dart b/lib/services/translate_service.dart new file mode 100644 index 0000000..71ecabc --- /dev/null +++ b/lib/services/translate_service.dart @@ -0,0 +1,125 @@ +import 'dart:convert'; +import 'package:http/http.dart' as http; +import '../models/language.dart'; +import 'settings_service.dart'; + +class TranslateException implements Exception { + final String message; + TranslateException(this.message); + @override + String toString() => message; +} + +class DetectResult { + final String language; + final int confidence; + DetectResult({required this.language, required this.confidence}); +} + +/// Kullanıcının kendi sunucusundaki LibreTranslate API'sine istek atar. +class TranslateService { + final SettingsService _settings; + TranslateService(this._settings); + + Future _baseUrl() async { + final url = await _settings.getServerUrl(); + if (url == null || url.isEmpty) { + throw TranslateException('Sunucu adresi ayarlanmamış. Ayarlar\'dan ekleyin.'); + } + return url; + } + + Map _withApiKey(Map body, String? apiKey) { + if (apiKey != null && apiKey.isNotEmpty) { + body['api_key'] = apiKey; + } + return body; + } + + String _errorFromBody(String body, int statusCode) { + try { + final decoded = jsonDecode(body); + if (decoded is Map && decoded['error'] != null) { + return decoded['error'].toString(); + } + } catch (_) { + // JSON değilse aşağıda genel mesaj kullanılır. + } + return 'Sunucu hatası (HTTP $statusCode)'; + } + + /// Sunucuya bağlanıp desteklenen dilleri getirir. + /// Hem "bağlantı testi" hem de dil listesini doldurmak için kullanılır. + Future> fetchLanguages() async { + final base = await _baseUrl(); + final apiKey = await _settings.getApiKey(); + final uri = Uri.parse('$base/languages').replace( + queryParameters: apiKey != null ? {'api_key': apiKey} : null, + ); + + final res = await http.get(uri).timeout(const Duration(seconds: 10)); + if (res.statusCode != 200) { + throw TranslateException(_errorFromBody(res.body, res.statusCode)); + } + final List data = jsonDecode(utf8.decode(res.bodyBytes)); + return data.map((e) => Language.fromJson(e as Map)).toList(); + } + + Future translate({ + required String text, + required String sourceLang, + required String targetLang, + }) async { + if (text.trim().isEmpty) return ''; + final base = await _baseUrl(); + final apiKey = await _settings.getApiKey(); + + final body = _withApiKey({ + 'q': text, + 'source': sourceLang, + 'target': targetLang, + 'format': 'text', + }, apiKey); + + final res = await http + .post( + Uri.parse('$base/translate'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ) + .timeout(const Duration(seconds: 15)); + + if (res.statusCode != 200) { + throw TranslateException(_errorFromBody(res.body, res.statusCode)); + } + final decoded = jsonDecode(utf8.decode(res.bodyBytes)); + return decoded['translatedText'] as String; + } + + Future detect(String text) async { + if (text.trim().isEmpty) return null; + final base = await _baseUrl(); + final apiKey = await _settings.getApiKey(); + + final body = _withApiKey({'q': text}, apiKey); + + final res = await http + .post( + Uri.parse('$base/detect'), + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), + ) + .timeout(const Duration(seconds: 10)); + + if (res.statusCode != 200) { + throw TranslateException(_errorFromBody(res.body, res.statusCode)); + } + final List data = jsonDecode(utf8.decode(res.bodyBytes)); + if (data.isEmpty) return null; + final best = data.first as Map; + return DetectResult( + language: best['language'] as String, + confidence: ((best['confidence'] ?? 0) as num).round(), + ); + } +} diff --git a/lib/theme/app_theme.dart b/lib/theme/app_theme.dart new file mode 100644 index 0000000..bb5346a --- /dev/null +++ b/lib/theme/app_theme.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class AppTheme { + // Ana vurgu rengi: derin çivit/indigo — çeviri/dil temasıyla uyumlu, sakin ve okunur. + static const _seed = Color(0xFF3849A6); + + static ThemeData get light => ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed(seedColor: _seed), + appBarTheme: const AppBarTheme(centerTitle: false, elevation: 0), + inputDecorationTheme: InputDecorationTheme( + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ); + + static ThemeData get dark => ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: _seed, + brightness: Brightness.dark, + ), + appBarTheme: const AppBarTheme(centerTitle: false, elevation: 0), + inputDecorationTheme: InputDecorationTheme( + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide.none, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ); +} diff --git a/linux/.gitignore b/linux/.gitignore new file mode 100644 index 0000000..d3896c9 --- /dev/null +++ b/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt new file mode 100644 index 0000000..65d9da4 --- /dev/null +++ b/linux/CMakeLists.txt @@ -0,0 +1,128 @@ +# Project-level configuration. +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "libretranslate") +# The unique GTK application identifier for this application. See: +# https://wiki.gnome.org/HowDoI/ChooseApplicationID +set(APPLICATION_ID "com.y1lm0z.libretranslate") + +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(SET CMP0063 NEW) + +# Load bundled libraries from the lib/ directory relative to the binary. +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Root filesystem for cross-building. +if(FLUTTER_TARGET_PLATFORM_SYSROOT) + set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) + set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) + set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) + set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +endif() + +# Define build configuration options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +# Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +# Application build; see runner/CMakeLists.txt. +add_subdirectory("runner") + +# Run the Flutter tool portions of the build. This must not be removed. +add_dependencies(${BINARY_NAME} flutter_assemble) + +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) + install(FILES "${bundled_library}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endforeach(bundled_library) + +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt new file mode 100644 index 0000000..d5bd016 --- /dev/null +++ b/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +# This file controls Flutter-level build steps. It should not be edited. +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000..e71a16d --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + + +void fl_register_plugins(FlPluginRegistry* registry) { +} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000..e0f0a47 --- /dev/null +++ b/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000..2e1de87 --- /dev/null +++ b/linux/flutter/generated_plugins.cmake @@ -0,0 +1,23 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/linux/runner/CMakeLists.txt b/linux/runner/CMakeLists.txt new file mode 100644 index 0000000..e97dabc --- /dev/null +++ b/linux/runner/CMakeLists.txt @@ -0,0 +1,26 @@ +cmake_minimum_required(VERSION 3.13) +project(runner LANGUAGES CXX) + +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. +apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the application ID. +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Add dependency libraries. Add any application-specific dependencies here. +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) + +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/linux/runner/main.cc b/linux/runner/main.cc new file mode 100644 index 0000000..e7c5c54 --- /dev/null +++ b/linux/runner/main.cc @@ -0,0 +1,6 @@ +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/linux/runner/my_application.cc b/linux/runner/my_application.cc new file mode 100644 index 0000000..100dc89 --- /dev/null +++ b/linux/runner/my_application.cc @@ -0,0 +1,148 @@ +#include "my_application.h" + +#include +#ifdef GDK_WINDOWING_X11 +#include +#endif + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; + char** dart_entrypoint_arguments; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Called when first Flutter frame received. +static void first_frame_cb(MyApplication* self, FlView* view) { + gtk_widget_show(gtk_widget_get_toplevel(GTK_WIDGET(view))); +} + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + MyApplication* self = MY_APPLICATION(application); + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + + // Use a header bar when running in GNOME as this is the common style used + // by applications and is the setup most users will be using (e.g. Ubuntu + // desktop). + // If running on X and not using GNOME then just use a traditional title bar + // in case the window manager does more exotic layout, e.g. tiling. + // If running on Wayland assume the header bar will work (may need changing + // if future cases occur). + gboolean use_header_bar = TRUE; +#ifdef GDK_WINDOWING_X11 + GdkScreen* screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) { + const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + use_header_bar = FALSE; + } + } +#endif + if (use_header_bar) { + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "libretranslate"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + } else { + gtk_window_set_title(window, "libretranslate"); + } + + gtk_window_set_default_size(window, 1280, 720); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + fl_dart_project_set_dart_entrypoint_arguments( + project, self->dart_entrypoint_arguments); + + FlView* view = fl_view_new(project); + GdkRGBA background_color; + // Background defaults to black, override it here if necessary, e.g. #00000000 + // for transparent. + gdk_rgba_parse(&background_color, "#000000"); + fl_view_set_background_color(view, &background_color); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + // Show the window when Flutter renders. + // Requires the view to be realized so we can start rendering. + g_signal_connect_swapped(view, "first-frame", G_CALLBACK(first_frame_cb), + self); + gtk_widget_realize(GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +// Implements GApplication::local_command_line. +static gboolean my_application_local_command_line(GApplication* application, + gchar*** arguments, + int* exit_status) { + MyApplication* self = MY_APPLICATION(application); + // Strip out the first argument as it is the binary name. + self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); + + g_autoptr(GError) error = nullptr; + if (!g_application_register(application, nullptr, &error)) { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; + } + + g_application_activate(application); + *exit_status = 0; + + return TRUE; +} + +// Implements GApplication::startup. +static void my_application_startup(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application startup. + + G_APPLICATION_CLASS(my_application_parent_class)->startup(application); +} + +// Implements GApplication::shutdown. +static void my_application_shutdown(GApplication* application) { + // MyApplication* self = MY_APPLICATION(object); + + // Perform any actions required at application shutdown. + + G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); +} + +// Implements GObject::dispose. +static void my_application_dispose(GObject* object) { + MyApplication* self = MY_APPLICATION(object); + g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); + G_OBJECT_CLASS(my_application_parent_class)->dispose(object); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; + G_APPLICATION_CLASS(klass)->local_command_line = + my_application_local_command_line; + G_APPLICATION_CLASS(klass)->startup = my_application_startup; + G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; + G_OBJECT_CLASS(klass)->dispose = my_application_dispose; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + // Set the program name to the application ID, which helps various systems + // like GTK and desktop environments map this running application to its + // corresponding .desktop file. This ensures better integration by allowing + // the application to be recognized beyond its binary name. + g_set_prgname(APPLICATION_ID); + + return MY_APPLICATION(g_object_new(my_application_get_type(), + "application-id", APPLICATION_ID, "flags", + G_APPLICATION_NON_UNIQUE, nullptr)); +} diff --git a/linux/runner/my_application.h b/linux/runner/my_application.h new file mode 100644 index 0000000..db16367 --- /dev/null +++ b/linux/runner/my_application.h @@ -0,0 +1,21 @@ +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, + my_application, + MY, + APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..582d01a --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,213 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" +sdks: + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..fd193ad --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,22 @@ +name: libretranslate_app +description: "Kendi LibreTranslate sunucunuza bağlanan çeviri uygulaması (Linux & Android)" +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: '>=3.0.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + http: ^1.2.2 + shared_preferences: ^2.3.2 + cupertino_icons: ^1.0.8 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^4.0.0 + +flutter: + uses-material-design: true diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..c9135cc --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:libretranslate/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}