This commit is contained in:
xuhang-x 2024-07-24 20:13:27 +08:00
parent 8594b0151a
commit 121e423c8b
15 changed files with 748 additions and 16 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -47,12 +47,12 @@ post_install do |installer|
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
'$(inherited)',
## dart: PermissionGroup.camera
'PERMISSION_CAMERA=1',
## dart: PermissionGroup.photos
'PERMISSION_PHOTOS=1',
## dartPermissionGroup.photosAddOnly
'PERMISSION_PHOTOS_ADD_ONLY=1',
## dart: PermissionGroup.appTrackingTransparency
'PERMISSION_APP_TRACKING_TRANSPARENCY=1',
]

View File

@ -31,6 +31,8 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>This will enable you to take photos and recognize text within them for translation.</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need access to your photo album so you can save wallpapers from the app to your album</string>
<key>NSPhotoLibraryUsageDescription</key>

View File

@ -24,7 +24,7 @@ class CustomAppbar extends StatelessWidget implements PreferredSizeWidget {
Widget build(BuildContext context) {
return Container(
height: preferredSize.height,
padding: EdgeInsets.fromLTRB(10, ScreenUtil().statusBarHeight, 20, 0).w,
padding: EdgeInsets.fromLTRB(10, ScreenUtil().statusBarHeight, 10, 0).w,
color: backgroundColor ?? seedColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -35,14 +35,11 @@ class CustomAppbar extends StatelessWidget implements PreferredSizeWidget {
color: Colors.transparent,
child: InkWell(
onTap: onBackTap ?? () => Get.back(),
child: Padding(
padding: const EdgeInsets.all(10).w,
child: Image.asset(
Assets.iconBack,
width: 32.w,
height: 32.w,
color: Colors.white,
),
child: Image.asset(
Assets.iconBack,
width: 32.w,
height: 32.w,
color: Colors.white,
),
),
),
@ -60,7 +57,7 @@ class CustomAppbar extends StatelessWidget implements PreferredSizeWidget {
),
),
),
Container(width: 13.w),
Container(width: 32.w),
],
),
);

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class TPhotoPickerBottomSheet extends StatelessWidget {
const TPhotoPickerBottomSheet({super.key, required this.funCamera, required this.funGallery});
final Function() funCamera;
final Function() funGallery;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Container(
width: MediaQuery.of(context).size.width,
height: 120,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(26),
),
child: Column(
children: [
Expanded(
child: _buildItem('Open camera', funCamera),
),
const Divider(
height: 1,
thickness: 1,
color: Color(0xFFEDEDED),
),
Expanded(
child: _buildItem('Open gallery', funGallery),
),
],
),
),
);
}
Widget _buildItem(String text, Function() function) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
Get.back();
function();
},
child: SizedBox(
width: double.infinity,
child: Center(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Color(0xFF333333),
fontSize: 16,
fontWeight: FontWeight.w600,
)
),
),
),
),
);
}
}

View File

@ -0,0 +1,253 @@
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import 'package:wallpaperx/common/utils/shared_util.dart';
import 'package:wallpaperx/generated/assets.dart';
class PinCodeVerificationScreen extends StatefulWidget {
final Function callback;
final String checkPassword;
const PinCodeVerificationScreen({
super.key,
required this.callback,
required this.checkPassword,
});
@override
State<PinCodeVerificationScreen> createState() =>
_PinCodeVerificationScreenState();
}
class _PinCodeVerificationScreenState extends State<PinCodeVerificationScreen> {
var onTapRecognizer;
TextEditingController textEditingController = TextEditingController();
late StreamController<ErrorAnimationType> errorController;
bool hasError = false;
String currentText = "";
final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
final formKey = GlobalKey<FormState>();
@override
void initState() {
onTapRecognizer = TapGestureRecognizer()
..onTap = () {
Navigator.pop(context);
};
errorController = StreamController<ErrorAnimationType>();
super.initState();
}
@override
void dispose() {
errorController.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
key: scaffoldKey,
body: SafeArea(
child: Container(
margin: EdgeInsets.only(top: 94.w),
height: MediaQuery.of(context).size.height,
width: MediaQuery.of(context).size.width,
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Please input a',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 28.sp,
color: Colors.white,
),
),
Text(
'password',
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 28.sp,
color: Colors.white,
),
)
],
),
Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(15.r),
),
child: Image.asset(
Assets.imagesCustomLock,
width: 100.w,
height: 100.w,
),
),
],
),
SizedBox(height: 64.w),
Form(
key: formKey,
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8,
horizontal: 30,
).w,
child: PinCodeTextField(
appContext: context,
pastedTextStyle: TextStyle(
color: Colors.green.shade600,
fontWeight: FontWeight.bold,
),
dialogConfig: DialogConfig(platform: PinCodePlatform.iOS),
length: 4,
obscureText: true,
obscuringCharacter: '*',
animationType: AnimationType.fade,
pinTheme: PinTheme(
activeColor: const Color(0xff262626),
selectedColor: const Color(0xff262626),
selectedFillColor: Colors.grey,
inactiveFillColor: const Color(0xff262626),
errorBorderColor: const Color(0xff262626),
inactiveColor: const Color(0xff262626),
shape: PinCodeFieldShape.box,
borderRadius: BorderRadius.circular(30.r),
fieldHeight: 58.w,
fieldWidth: 58.w,
activeFillColor: hasError
? const Color(0xff262626)
: const Color(0xff262626),
),
showCursor: false,
animationDuration: const Duration(milliseconds: 300),
textStyle: TextStyle(
fontSize: 32.sp,
fontWeight: FontWeight.w700,
color: Colors.white,
height: 2.0,
),
backgroundColor: Colors.transparent,
enableActiveFill: true,
errorAnimationController: errorController,
controller: textEditingController,
keyboardType: TextInputType.number,
boxShadows: const [
BoxShadow(
offset: Offset(0, 1),
color: Colors.black12,
blurRadius: 30,
),
],
onCompleted: (v) {},
onChanged: (value) {
setState(() {
currentText = value;
});
},
beforeTextPaste: (text) {
setState(() {
currentText = text!;
});
return true;
},
)),
),
Visibility(
visible: widget.checkPassword == "",
child: Text(
"First input as password",
style: TextStyle(
color: Colors.red,
fontSize: 14.sp,
),
),
),
GestureDetector(
onTap: () {
formKey.currentState?.validate();
if (currentText.length == 4 && widget.checkPassword == "") {
UPCache.getInstance()
.setData("custom_password", currentText);
widget.callback();
} else if (currentText.length != 4 ||
currentText != widget.checkPassword) {
errorController.add(ErrorAnimationType
.shake); // Triggering error shake animation
setState(() {
hasError = true;
textEditingController.clear();
});
} else {
widget.callback();
setState(() {
hasError = false;
});
}
},
child: Container(
margin: const EdgeInsets.symmetric(
vertical: 24,
horizontal: 40,
).w,
padding: const EdgeInsets.symmetric(
vertical: 17,
).w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.r),
gradient: const LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Color(0xffBEEF32),
Color(0xff2795E5),
Color(0xff8041FD),
],
),
),
child: Center(
child: Text(
"Confirm",
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.bold),
),
),
),
),
GestureDetector(
child: Text(
"Clear",
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
),
),
onTap: () {
textEditingController.clear();
},
),
],
),
),
),
);
}
}

View File

@ -0,0 +1,41 @@
import 'package:wallpaperx/common/storage/hive_storage.dart';
import 'package:wallpaperx/entity/image_model.dart';
class CustomData {
///
CustomData._();
///
static final CustomData _instance = CustomData._();
///
factory CustomData() {
return _instance;
}
///
/// , main函数中这个盒子已经打开,
final _box = getCustomBox();
///
List<ImageModel> getWallpaperData() {
return _box.values.toList();
}
///
Future<int> setWallpaperData(ImageModel wallpaperData) async {
return await _box.add(wallpaperData);
}
///
Future<void> delete(index) async {
await _box.deleteAt(index);
await _box.flush();
}
///
Future<void> clear() async {
await _box.clear();
await _box.flush();
}
}

View File

@ -5,6 +5,7 @@ import 'package:wallpaperx/entity/tags_model.dart';
const favoriteBox = 'favoriteBox';
const historyBox = 'historyBox';
const customBox = 'customBox';
Future initHive() async {
//
@ -16,6 +17,7 @@ Future initHive() async {
//
await Hive.openBox<ImageModel>(favoriteBox);
await Hive.openBox<ImageModel>(historyBox);
await Hive.openBox<ImageModel>(customBox);
}
///
@ -26,4 +28,9 @@ Box<ImageModel> getFavoriteBox() {
///
Box<ImageModel> getHistoryBox() {
return Hive.box<ImageModel>(historyBox);
}
///
Box<ImageModel> getCustomBox() {
return Hive.box<ImageModel>(customBox);
}

View File

@ -46,6 +46,8 @@ class Assets {
static const String iconUnFavorite = 'assets/icon/un_favorite.png';
static const String iconUp = 'assets/icon/up.png';
static const String imagesCollectionSelected = 'assets/images/collection_selected.png';
static const String imagesCustomLock = 'assets/images/custom_lock.png';
static const String imagesCustomSelected = 'assets/images/custom_selected.png';
static const String imagesRecommendBottomBackground = 'assets/images/recommend_bottom_background.png';
static const String imagesRecommendSelected = 'assets/images/recommend_selected.png';
static const String imagesRecommendTopBackground = 'assets/images/recommend_top_background.png';

View File

@ -0,0 +1,230 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:crop_your_image/crop_your_image.dart';
import 'package:flip_card/flip_card_controller.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:pin_code_fields/pin_code_fields.dart';
import 'package:uuid/uuid.dart';
import 'package:wallpaperx/common/components/dialog/remind_dialog.dart';
import 'package:wallpaperx/common/components/easy_loading.dart';
import 'package:wallpaperx/common/components/navigation_bar/custom_appbar.dart';
import 'package:wallpaperx/common/components/photo_picker_bottom_sheet.dart';
import 'package:wallpaperx/common/components/view_state_widget.dart';
import 'package:wallpaperx/common/storage/custom_data.dart';
import 'package:wallpaperx/common/utils/device_info_util.dart';
import 'package:wallpaperx/common/utils/log_print.dart';
import 'package:wallpaperx/common/utils/permission_util.dart';
import 'package:wallpaperx/common/utils/shared_util.dart';
import 'package:wallpaperx/entity/image_model.dart';
import 'package:wallpaperx/routes/app_pages.dart';
class CustomController extends GetxController {
static CustomController get to => Get.find<CustomController>();
late ScrollController scrollController;
late ViewState viewState;
RxList customList = [].obs;
String password = "";
TextEditingController textEditingController = TextEditingController();
late StreamController<ErrorAnimationType> errorController;
late FlipCardController flipCardController;
bool hasError = false;
@override
void onInit() {
super.onInit();
errorController = StreamController<ErrorAnimationType>();
scrollController = ScrollController();
flipCardController = FlipCardController();
password = UPCache.getInstance().get("custom_password")??"";
getCustomList();
}
@override
void onClose() {
scrollController.dispose();
super.onClose();
}
void getCustomList() {
customList.clear();
customList.addAll(CustomData().getWallpaperData().reversed.toList());
LogPrint.d(customList.length);
refreshCustomList();
}
void refreshCustomList() {
viewState = customList.isNotEmpty ? ViewState.normal : ViewState.empty;
refresh();
}
///
void toImageDetail(int position) {
Get.toNamed(AppPages.wallpaperDetail, arguments: {
'position': position,
'isSetHistory': false,
'wallpaperList': customList,
});
}
///
void onLongPressImage(int position) {
Get.dialog(
barrierDismissible: false,
RemindDialog(
content: 'Are you sure you want to delete the record?',
confirmOnTap: () {
//
int indexToRemoveFromDb =
CustomData().getWallpaperData().length - 1 - position;
CustomData().delete(indexToRemoveFromDb);
customList.removeAt(position);
refreshCustomList();
},
),
);
}
Future<void> toPhotos() async {
Get.bottomSheet(
isScrollControlled: true,
TPhotoPickerBottomSheet(
funCamera: () async {
bool result =
await PermissionUtil.checkPermission([Permission.camera]);
if (!result) return;
_openCameraGallery(ImageSource.camera);
},
funGallery: () async {
Permission permission = Permission.photos;
if (Platform.isAndroid) {
int sdkInt = await DeviceInfoUtil.getAndroidSystemVersion();
if (sdkInt <= 32) {
permission = Permission.storage;
} else {
permission = Permission.photos;
}
}
bool result = await PermissionUtil.checkPermission([permission]);
if (!result) return;
_openCameraGallery(ImageSource.gallery);
},
),
);
}
Future<void> _openCameraGallery(ImageSource source) async {
final ImagePicker picker = ImagePicker();
final XFile? photo = await picker.pickImage(source: source);
if (photo != null) {
final controller = CropController();
Uint8List image = await photo.readAsBytes();
Get.to(
() => Scaffold(
backgroundColor: Colors.black,
appBar: const CustomAppbar(''),
body: SafeArea(
top: false,
child: Column(
children: [
Expanded(
child: Crop(
baseColor: Colors.black,
image: image,
controller: controller,
onCropped: (image) {
setFile(image);
LogPrint.d('裁剪后的图片:${image.lengthInBytes}');
},
),
),
Row(
children: [
Expanded(child: Container()),
GestureDetector(
onTap: controller.crop,
child: Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 46,
).w,
padding: const EdgeInsets.symmetric(
horizontal: 35,
vertical: 8,
).w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30.r),
gradient: const LinearGradient(
begin: Alignment.topRight,
end: Alignment.bottomLeft,
colors: [
Color(0xffBEEF32),
Color(0xff2795E5),
Color(0xff8041FD),
],
),
),
child: Center(
child: Text(
"Apply",
style: TextStyle(
color: Colors.white,
fontSize: 18.sp,
fontWeight: FontWeight.bold),
),
),
),
)
],
),
],
),
),
),
);
}
}
Future setFile(Uint8List image) async {
try {
String uuid = const Uuid().v4();
Directory directory = await getApplicationDocumentsDirectory();
File file = File('${directory.path}/$uuid.txt');
//
await file.writeAsBytes(image);
//
ImageModel model = ImageModel(imageUrl: file.path);
CustomData().setWallpaperData(model);
Get.back();
getCustomList();
} catch (e) {
toast(e.toString());
}
}
Uint8List? readImage(String filePath) {
try {
final file = File(filePath);
Uint8List u8 = file.readAsBytesSync();
return u8;
} catch (e) {
return null;
}
}
///
void flipCard() {
flipCardController.toggleCard();
}
}

View File

@ -0,0 +1,129 @@
import 'dart:typed_data';
import 'package:flip_card/flip_card.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import 'package:get/get.dart';
import 'package:wallpaperx/common/components/navigation_bar/custom_appbar.dart';
import 'package:wallpaperx/common/components/pin_code_verification_screen.dart';
import 'package:wallpaperx/common/components/view_state_widget.dart';
import 'package:wallpaperx/entity/image_model.dart';
import 'package:wallpaperx/generated/assets.dart';
import 'custom_controller.dart';
class CustomView extends GetView<CustomController> {
const CustomView({super.key});
@override
Widget build(BuildContext context) {
Get.put(CustomController());
return Scaffold(
backgroundColor: Colors.black,
body: FlipCard(
flipOnTouch: false,
controller: controller.flipCardController,
fill: Fill.fillBack,
side: CardSide.FRONT,
direction: FlipDirection.HORIZONTAL,
front: PinCodeVerificationScreen(
callback: controller.flipCard,
checkPassword: controller.password,
),
back: _buildCustomWidget(context),
),
);
}
Widget _buildCustomWidget(context) {
return Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage(Assets.imagesSettingBackground),
fit: BoxFit.cover,
),
),
child: Column(
children: [
CustomAppbar(
"Custom",
backgroundColor: Colors.transparent,
backWidget: Container(width: 32.w),
titleStyle: TextStyle(
color: Colors.white,
fontSize: 24.sp,
fontWeight: FontWeight.w600,
),
),
Expanded(child: GetBuilder<CustomController>(
builder: (logic) {
return ViewStateWidget(
viewState: controller.viewState,
child: MediaQuery.removePadding(
context: context,
removeTop: true,
child: Scrollbar(
controller: controller.scrollController,
child: MasonryGridView.count(
controller: controller.scrollController,
itemCount: controller.customList.length,
crossAxisCount: 2,
mainAxisSpacing: 15.w,
crossAxisSpacing: 15.w,
padding: const EdgeInsets.fromLTRB(15, 20, 15, 0).w,
physics: const BouncingScrollPhysics(),
itemBuilder: (context, index) {
ImageModel item = controller.customList[index];
Uint8List? image =
controller.readImage(item.imageUrl ?? "");
if (image == null) return Container();
return GestureDetector(
onTap: () {
Get.to(
() => Scaffold(
backgroundColor: Colors.black,
appBar: const CustomAppbar(''),
body: Container(
alignment: Alignment.center,
child: Image.memory(image),
),
),
);
},
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(13.r),
),
child: Image.memory(image),
),
);
},
),
),
),
);
},
))
],
),
),
Positioned(
right: 10.w,
bottom: MediaQuery.of(context).padding.bottom + 80.w,
child: FloatingActionButton(
backgroundColor: Colors.grey,
onPressed: controller.toPhotos,
child: const Icon(Icons.add),
),
)
],
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:wallpaperx/common/utils/shared_util.dart';
import 'package:wallpaperx/config/app_tracking_transparency_manager.dart';
import 'package:wallpaperx/entity/userinfo_model.dart';
import 'package:wallpaperx/generated/assets.dart';
import 'package:wallpaperx/page/custom/custom_view.dart';
import 'package:wallpaperx/page/library/library_view.dart';
import 'package:wallpaperx/page/recommend/recommend_view.dart';
import 'package:wallpaperx/page/settings/settings_view.dart';
@ -15,6 +16,7 @@ class HomeController extends GetxController with WidgetsBindingObserver {
final pages = [
PageItem(Assets.imagesRecommendSelected, const RecommendView()),
PageItem(Assets.imagesCollectionSelected, const LibraryView()),
PageItem(Assets.imagesCustomSelected, const CustomView()),
PageItem(Assets.imagesSettingSelected, const SettingsView()),
];
late PageController pageController;

View File

@ -85,9 +85,7 @@ class WallpaperDetailController extends GetxController {
filePath = savePath;
bool canSave = true;
if (Platform.isIOS) {
canSave = await PermissionUtil.checkPermission(
[Permission.photosAddOnly],
);
canSave = await PermissionUtil.checkPermission([Permission.photos]);
}
if (canSave) {
final result =

View File

@ -101,6 +101,11 @@ dependencies:
app_tracking_transparency: ^2.0.5
gradient_borders: ^1.0.1
stacked_animated_list: ^1.0.1
pin_code_fields: ^8.0.1
image_picker: ^1.1.2
uuid: ^4.4.2
flutter_file_dialog: ^3.0.2
crop_your_image: ^1.1.0
# Firebase
firebase_core: ^2.32.0